Skip to content

exposing handle creation functions and fixed issue in reading multi line response - #2

Open
pucsdian wants to merge 3 commits into
flipstone:mainfrom
pucsdian:main
Open

exposing handle creation functions and fixed issue in reading multi line response #2
pucsdian wants to merge 3 commits into
flipstone:mainfrom
pucsdian:main

Conversation

@pucsdian

@pucsdian pucsdian commented Apr 1, 2025

Copy link
Copy Markdown

$1. Exposed below 3 functions to have better control on ftp handle's life-cycle.

  1. createSIOHandle
  2. createTLSConnection
  3. connectTLS

$2. Fixed issue in reading multi line response
eg : for response like below
'220-First Line
220-Second Line
220 Third Line'

Expected all 3 lines but getting only 2 as code checks for only code of 2 lines.

But the check should follow https://datatracker.ietf.org/doc/html/rfc959#page-36
Thus the format for multi-line replies is that the first line will begin with the exact required reply code, followed immediately by a Hyphen, "-" (also known as Minus), followed by text. The last line will begin with the same code, followed immediately by Space , optionally some text, and the Telnet end-of-line code.

@ysangkok

ysangkok commented Apr 2, 2025

Copy link
Copy Markdown

how do you test this?

@telser telser left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall seems okay, but again second the testing, though we don't have a suite today.

let newLines = lines <> [C.dropWhile (== ' ') nextLine]
nextCode = C.take 3 nextLine
if nextCode == code
isLastLine = C.isPrefixOf (code <> " ") nextLine -- Ref for reading multiline response : https://datatracker.ietf.org/doc/html/rfc959#page-36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable on the face to me, but I do really wish we had a test suite for these kinds of things.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have test suite now, but I faced this issue with one FTP server where welcome banner is like provided example.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the hostname and port of the FTP server?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't share the details. But you can use below python ftp server to test it,

you need to add pyftpdlib

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer

authorizer = DummyAuthorizer()
authorizer.add_user("user", "12345", ".", perm="elr") 

class CustomHandler(FTPHandler):
    def on_connect(self):
        self.banner = (
            "220-Welcome to the Python FTP Server!\n"
            "220-This is a test server with a multiline banner.\n"
            "220-Feel free to browse or upload files.\n"
            "220 Have fun!"
        )
        super().on_connect()

handler = CustomHandler
handler.authorizer = authorizer

server = FTPServer(("127.0.0.1", 2121), handler)
print("FTP Server running on port 2121...")
server.serve_forever()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the snippet, I tried it with pyftpdlib version 2.0.1. I think the 220 on the first line needs to be removed, otherwise it is duplicated.

So I tested with

            "Welcome to the Python FTP Server!\n"
            "220-This is a test server with a multiline banner.\n"
            "220-Feel free to browse or upload files.\n"
            "220 Have fun!"

This emits

220-Welcome to the Python FTP Server!
220-This is a test server with a multiline banner.
220-Feel free to browse or upload files.
220 Have fun!
220

when connected to with nc localhost 2121.

I tried testing with the program

module Main where

import Network.FTP.Client

main :: IO ()
main = withFTP "127.0.0.1" 2121 $ \h welcome -> do
    putStrLn "Connected, printing welcome"
    print welcome
    putStrLn "Now doing login"
    login h "user" "12345"
    putStrLn "Did login"

On main, this progam surprisingly succeeds, with the output

Connected, printing welcome
220 Welcome to the Python FTP Server!
220-This is a test server with a multiline banner.
Now doing login
Did login

I think it mistakes the additional lines for successful login responses. On this PR, it gives the following output:

Connected, printing welcome
220 Welcome to the Python FTP Server!
220-This is a test server with a multiline banner.
220-Feel free to browse or upload files.
220 Have fun!
Now doing login
example-exe: Uncaught exception ftp-client-0.5.1.6-inplace:Network.FTP.Client.FTPException:

UnsuccessfulException 331 Username ok, send password.

While handling UnsuccessfulException 331 Username ok, send password.

So it seems that ftp-client doesn't understand the type of exchange in section 7 of the RFC.

Did you also encounter this issue? I am surprised that you didn't hit this issue, because you said you had tested with this program.

@pucsdian pucsdian May 22, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I faced same issue. bcoz python server by default sending 200<space> after banner lines so we should not add 200<space> in our banner at beginning of last line.

Please try with below snippet. (without changing pytpdlib)

"Welcome to the Python FTP Server!\n"
"220-This is a test server with a multiline banner.\n"
"220-Feel free to browse or upload files. Have fun!\n"

I am surprised that you didn't hit this issue, because you said you had tested with this program => I forgot to tell you that I have made some changes in the python library so that it sends the banner as we have provided there, sorry for that.
Please check this
lib/python3.8/site-packages/pyftpdlib/handlers.py

if not self._closed and not self._closing:
          if len(self.banner) <= 75:
                self.respond(f"220 {self.banner!s}")
            else:
                self.push(f'220-{self.banner!s}\r\n')
              #  self.respond('220 ')
 

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel comfortable merging this, since it seems like it might provoke other issues, like the missing handling of the 331 code. Also, I am not comfortable with changing other libraries to test this library.

But I think the idea is ok, it seems like multi-line banners are common and should be supported. It's just that if we merge this, we should also make sure that the example scenario I posted above (with no modifications to pyftpdlib) is handled in this library. That way, at least we know that this basic use case keeps working.

Currently, since we don't have a patch for code 331 support, I think we're blocked on that.

onslaughtq added a commit that referenced this pull request Aug 22, 2026
Follows bounded-text, beeline, shrubbery, orb, rollbar-haskell and
haskell-non-empty-text, which all carry a manual ci flag holding the strict
ghc-options. Two deviations from that list: no henforcer plugin, since this
repo has no henforcer.toml and its open imports would fail immediately, and
no -Wmissing-import-lists, which would rewrite thirteen imports in the file
PR #2 also edits.

The flag is manual and defaults off, so nothing reaches Hackage consumers.
It is enabled for all three packages in stack-base.yaml, so every rung of
the matrix builds with -Werror. Libraries get no ghc-options at all when the
flag is off, as before -- an -O2 in the else branch would have imposed it on
downstream users, and cabal check says so.

Roughly fifty warnings across the four source files. Most were mechanical:
unused imports and do-binds, shadowed names, missing local signatures. Three
were not:

The record update at connectTLS was incomplete because TLSSettings is a sum
type and settingDisableCertificateValidation only exists on TLSSettingsSimple.
Rebuilding just that constructor keeps it working on crypton-connection 0.3,
which lacks the settingClientSupported field a positional call would need.

Producer and Consumer are deprecated conduit synonyms, five of them in
exported signatures. They expand to `forall i. ConduitT i o m ()` and
`forall o. ConduitT i o m r`, not to ConduitT () o m () and ConduitT i Void
m r -- those are Source and Sink. Rewriting them with the type variables left
free keeps the exported types identical; pinning them to ()/Void would have
narrowed the API and broken the internal fusion in mlsd and stor.

MonadResource is in five exported signatures but is re-exported by Conduit,
so the direct resourcet import counted as unused and -Wunused-packages then
rejected the dependency. Taking the name from resourcet explicitly and hiding
it from the Conduit import keeps the dependency and its version bound honest.

transformers comes out of ftp-client and containers out of example; neither
was imported anywhere. Deriving Typeable is dropped -- a no-op since GHC 7.10
that only GHC 9.12 warns about -- and acct, pbsz, prot, ccc and auth are now
exported rather than sitting unused, matching every other command wrapper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants