packages feed

ftp-client 0.5.1.6 → 0.6.0.0

raw patch · 7 files changed

+1703/−909 lines, 7 filesdep +directorydep +henforcerdep −transformersdep ~basedep ~containerssetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: directory, henforcer

Dependencies removed: transformers

Dependency ranges changed: base, containers

API changes (from Hackage documentation)

+ Network.FTP.Client: TLSContext :: TLSSettings -> String -> Int -> TLSContext
+ Network.FTP.Client: [tlsContextHost] :: TLSContext -> String
+ Network.FTP.Client: [tlsContextPort] :: TLSContext -> Int
+ Network.FTP.Client: [tlsContextSettings] :: TLSContext -> TLSSettings
+ Network.FTP.Client: acct :: MonadIO m => Handle -> String -> m FTPResponse
+ Network.FTP.Client: connectTLS :: MonadIO m => TLSSettings -> Handle -> String -> Int -> m Connection
+ Network.FTP.Client: createSIOHandle :: (MonadIO m, MonadMask m) => String -> Int -> m Handle
+ Network.FTP.Client: createTLSConnection :: (MonadIO m, MonadMask m) => TLSSettings -> String -> Int -> m (FTPResponse, Connection)
+ Network.FTP.Client: data PendingCompletion
+ Network.FTP.Client: data TLSContext
+ Network.FTP.Client: drainPendingCompletion :: (MonadIO m, MonadCatch m) => Handle -> PendingCompletion -> m ()
+ Network.FTP.Client: getAllLineResp :: (MonadIO m, MonadCatch m) => Handle -> m ByteString
+ Network.FTP.Client: getLineRespMaybe :: Handle -> IO (Maybe ByteString)
+ Network.FTP.Client: maxReplyLineLength :: Int
+ Network.FTP.Client: newPendingCompletion :: MonadIO m => m PendingCompletion
+ Network.FTP.Client: pbsz :: MonadIO m => Handle -> Int -> m FTPResponse
+ Network.FTP.Client: prot :: MonadIO m => Handle -> ProtType -> m FTPResponse
+ Network.FTP.Client: requireTLSContext :: MonadIO m => Handle -> m TLSContext
+ Network.FTP.Client: toNetworkAscii :: ByteString -> ByteString
+ Network.FTP.Client: withFTPSSettings :: (MonadMask m, MonadIO m) => TLSSettings -> String -> Int -> (Handle -> FTPResponse -> m a) -> m a
- Network.FTP.Client: TLS :: Security
+ Network.FTP.Client: TLS :: TLSContext -> Security
- Network.FTP.Client: createSendDataCommand :: (MonadIO m, MonadMask m) => Handle -> PortActivity -> FTPCommand -> m Handle
+ Network.FTP.Client: createSendDataCommand :: (MonadIO m, MonadMask m) => Handle -> PortActivity -> PendingCompletion -> FTPCommand -> m Handle
- Network.FTP.Client: createTLSSendDataCommand :: (MonadIO m, MonadMask m) => Handle -> PortActivity -> FTPCommand -> m Connection
+ Network.FTP.Client: createTLSSendDataCommand :: (MonadIO m, MonadMask m) => Handle -> PortActivity -> PendingCompletion -> FTPCommand -> m Connection
- Network.FTP.Client: tlsHandleImpl :: Connection -> Handle
+ Network.FTP.Client: tlsHandleImpl :: TLSContext -> Connection -> Handle

Files

+ CHANGELOG.md view
@@ -0,0 +1,130 @@+# Changelog for ftp-client++## 0.6.0.0++**Breaking change.** `withFTPS` now verifies the server's certificate chain and+host name. Validation was previously disabled, and a caller had no way to enable+it. Reported by @ysangkok in+<https://github.com/flipstone/ftp-client/pull/1>, which proposed the same fix and+was approved but closed unmerged; this completes that change.++If you connect to a server whose certificate cannot be validated, that+connection will now fail. Use the new `withFTPSSettings` with+`settingDisableCertificateValidation` set to keep the previous behaviour+deliberately.++* `withFTPSSettings` takes `Connection.TLSSettings`, for callers who need to+  choose their own.++* Data connections now authenticate against the host the control connection was+  opened to. They previously used the *local* end of the data socket, which no+  server certificate can match. This is why enabling validation on the control+  connection alone was not sufficient: PR #1 changed only that, and on its own+  would have left every FTPS data transfer unable to validate.++* `Security` now carries a `TLSContext` (settings, host, port) so a data+  connection can reproduce the control connection's protection. `connectTLS`,+  `createTLSConnection`, `withTLSHandle` and `tlsHandleImpl` take the settings+  or context they need.++* Reply lines are now length limited on both control connections.+  `connectionGetLine` was called with `maxBound`, so a server that never sent a+  newline could exhaust memory before authentication. The clear-channel handle+  used unbounded `hGetLine`, which left `withFTP` replies -- and the plaintext+  greeting and `AUTH TLS` reply that `createTLSConnection` reads before+  authenticating -- without any limit at all. Both now apply+  `maxReplyLineLength`, which is exported, and raise+  `Network.Connection.LineTooLong` past it.++* IO failures during a transfer are no longer reported as a completed one.+  `recvAll`, `getAllLineResp` and `getMlsxResponse` turned any `IOError` into a+  clean end of data, so a reset or timed-out connection produced a truncated+  result that a caller could not distinguish from a whole one. End of input is+  now distinguished from failure, and only end of input terminates a read.++* Fixed three descriptor leaks: `createTLSConnection` on a refused greeting,+  rejected `AUTH TLS` or failed handshake; the data handshake, where+  `socketToHandle` had already invalidated the socket the release closed; and+  the active-mode listening socket, which was never closed on success.++* A data transfer that does not complete normally now still consumes the+  server's completion reply. Left unread it became the answer to the next+  command, and every reply after that belonged to the previous command.++  That drain is now conditional on a completion reply actually being pending.+  A transfer the server rejects outright -- `PBSZ`, `PROT`, `PASV` or the+  transfer command itself -- fails with its error reply already consumed, and+  nothing further is coming, so draining blocked until the server gave up on+  the connection. `PendingCompletion`, `newPendingCompletion` and+  `drainPendingCompletion` are exported, and `createSendDataCommand` and+  `createTLSSendDataCommand` take a `PendingCompletion`.++* `TYPE A` transfers now send CRLF as RFC 959 requires. `sendType TA` doubled a+  CR that was already there and appended a record the input did not have, and+  `sendLine` sent a bare LF.++* `ccc` and `auth` are removed. CCC cannot work here -- there is no way to+  downgrade our side of the connection, so the control connection would+  desynchronise -- and `auth` on its own tells the server to expect a handshake+  that never happens. Both remain reachable as `FTPCommand` constructors.++* `getLineRespMaybe`, `getAllLineResp`, `toNetworkAscii` and+  `maxReplyLineLength` are now exported.++## 0.5.3.1++* Enable the `henforcer` plugin and `fourmolu` under the `ci` flag. Imports are+  now qualified per the house style and the source is fourmolu formatted;+  neither changes the API.++## 0.5.3.0++* Export `acct`, `pbsz`, `prot`, `ccc` and `auth`. These command wrappers were+  defined but never exported, unlike every other command wrapper in the module.++* Drop the `transformers` dependency. The only module it supplied,+  `Control.Monad.IO.Class`, has been in `base` since 4.9.++* Stop deriving `Typeable` for `FTPException`. It has been a no-op since GHC+  7.10 and GHC 9.12 warns about it.++## 0.5.2.0++* Expose `createSIOHandle`, `createTLSConnection` and `connectTLS` so callers can+  manage the handle lifecycle themselves rather than going through `withFTP` and+  `withFTPS`. Thanks to @pucsdian.++* Fix multiline response parsing. A response was terminated at the first+  continuation line whose first three bytes matched the response code, so a reply+  such as `220-First` / `220-Second` / `220 Third` was truncated to two lines. Per+  [RFC 959](https://datatracker.ietf.org/doc/html/rfc959#page-36) only the code+  followed by a space ends a multiline reply; the code followed by a hyphen+  continues it. A final line consisting of the bare code is also accepted, for+  servers that omit the trailing space. Thanks to @pucsdian.++## 0.5.1.8++* Fix a crash on short response lines. `getResponse` called `head` on the bytes+  following the response code, so a line shorter than four bytes failed with+  `Prelude.head: empty list` instead of an `FTPException`. Response lines that do+  not begin with a three digit code now raise `BadProtocolResponseException`.++* Fix a hang when the server closes the connection partway through a multiline+  response. The read loop had no terminating condition other than the closing+  code, so it never returned. An exhausted stream now raises+  `BadProtocolResponseException`. The loop terminates, and a reply the server+  never finished is reported as bad rather than handed back as though it were+  complete -- which would have let a truncated `220-` greeting read as a+  successful 220 and let `withFTP` proceed against a dead control connection.++## 0.5.1.7++* Correct the `base` bound. The package claimed `>= 4.8`, i.e. support back to+  GHC 7.10, but no compiler that old can build it because `crypton-connection`+  does not exist there. The bound is now `>= 4.16`, the oldest GHC that is+  actually tested, and `tested-with` records the full set.++## Earlier releases++Prior to 0.5.1.7 this package had no changelog. See the git history at+<https://github.com/flipstone/ftp-client> for changes in earlier versions.
README.md view
@@ -13,9 +13,24 @@ ```  ## Secured with TLS++`withFTPS` verifies the server's certificate chain and host name, so a server+that cannot be validated is refused.+ ```haskell withFTPS "ftps.server.com" 21 $ \h welcome -> do     print welcome     login h "username" "password"     print =<< nlst h []+```++To talk to a server whose certificate cannot be validated, pass your own+settings. Disabling validation leaves the connection encrypted but not+authenticated, so anyone on the network path can read the credentials and alter+transferred data:++```haskell+let insecure = def { settingDisableCertificateValidation = True }+withFTPSSettings insecure "ftps.server.com" 21 $ \h welcome ->+    print welcome ```
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
ftp-client.cabal view
@@ -1,43 +1,99 @@-name:                ftp-client-version:             0.5.1.6-synopsis:            Transfer files with FTP and FTPS-description:         ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS.-homepage:            https://github.com/flipstone/ftp-client-license:             PublicDomain-maintainer:          Flipstone Technology Partners-license-file:        LICENSE-author:              Megan Robinson-category:            Web-build-type:          Simple-extra-source-files:  README.md-cabal-version:       >=1.10+cabal-version: 1.12 -library-  hs-source-dirs:      src-  exposed-modules:     Network.FTP.Client-  default-language:    Haskell2010-  default-extensions:  OverloadedStrings-  build-depends:       base >= 4.8 && < 5-                     , bytestring >= 0.10.8.2 && < 0.13-                     , network >= 2.6.3.6 && < 3.3-                     , attoparsec >= 0.10 && < 0.15-                     , crypton-connection >= 0.3 && < 0.5-                     , transformers >= 0.5.6.2 && < 0.7-                     , exceptions >= 0.10.3 && < 0.11-                     , containers >= 0.5.11.0 && < 0.8-                     , data-default-class >= 0.1.2.0 && < 0.2+-- This file has been generated from package.yaml by hpack version 0.39.1.+--+-- see: https://github.com/sol/hpack -test-suite ftp-client-test-  type:                exitcode-stdio-1.0-  hs-source-dirs:      test-  main-is:             test.hs-  build-depends:       base >=4.11.1.0 && < 5-                     , bytestring >=0.10.8.2 && <0.13-                     , hspec >= 2.7-                     , ftp-client -any-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N-  default-language:    Haskell2010+name:           ftp-client+version:        0.6.0.0+synopsis:       Transfer files with FTP and FTPS+description:    ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS.+category:       Web+homepage:       https://github.com/flipstone/ftp-client+author:         Megan Robinson+maintainer:     Flipstone Technology Partners+license:        PublicDomain+license-file:   LICENSE+build-type:     Simple+tested-with:+    GHC == 9.2.8+  , GHC == 9.4.8+  , GHC == 9.6.7+  , GHC == 9.8.4+  , GHC == 9.10.3+  , GHC == 9.12.4+extra-source-files:+    README.md+    CHANGELOG.md+    henforcer.toml  source-repository head-  type:     git+  type: git   location: https://github.com/flipstone/ftp-client++flag ci+  description: More strict ghc options used for development and ci, not intended for end-use.+  manual: True+  default: False++library+  exposed-modules:+      Network.FTP.Client+  other-modules:+      Paths_ftp_client+  hs-source-dirs:+      src+  default-extensions:+      OverloadedStrings+  build-depends:+      attoparsec >=0.10 && <0.15+    , base >=4.16 && <5+    , bytestring >=0.10.8.2 && <0.13+    , containers >=0.5.11.0 && <0.8+    , crypton-connection >=0.3 && <0.5+    , data-default-class >=0.1.2.0 && <0.3+    , exceptions >=0.10.3 && <0.11+    , network >=2.6.3.6 && <3.3+  default-language: Haskell2010+  if flag(ci)+    ghc-options: -O2 -Wall -Werror -Wcompat -Widentities -Winvalid-haddock -Wmissing-local-signatures -Wmissing-export-lists -Wpartial-fields -Wmissed-specialisations -Wno-implicit-prelude -Wno-safe -Wno-unsafe -Wunused-packages+    if impl (ghc < 9.4)+      ghc-options: -Wno-unticked-promoted-constructors+    if impl (ghc >= 9.4)+      ghc-options: -Wimplicit-lift -Woperator-whitespace -Wredundant-bang-patterns -Wredundant-strictness-flags+    if impl (ghc >= 9.8)+      ghc-options: -Wincomplete-export-warnings -Wmissing-poly-kind-signatures -Wterm-variable-capture+    if impl (ghc >= 9.10)+      ghc-options: -Wdefaulted-exception-context+    if impl (ghc >= 9.6)+      ghc-options: -fplugin Henforcer+      build-depends:+          henforcer++test-suite ftp-client-test+  type: exitcode-stdio-1.0+  main-is: test.hs+  other-modules:+      Paths_ftp_client+  hs-source-dirs:+      test+  build-depends:+      base >=4.16 && <5+    , bytestring >=0.10.8.2 && <0.13+    , crypton-connection >=0.3 && <0.5+    , directory ==1.3.*+    , ftp-client+    , hspec >=2.7+  default-language: Haskell2010+  if flag(ci)+    ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Werror -Wcompat -Widentities -Winvalid-haddock -Wmissing-local-signatures -Wmissing-export-lists -Wpartial-fields -Wmissed-specialisations -Wno-implicit-prelude -Wno-safe -Wno-unsafe+    if impl (ghc < 9.4)+      ghc-options: -Wno-unticked-promoted-constructors+    if impl (ghc >= 9.4)+      ghc-options: -Wimplicit-lift -Woperator-whitespace -Wredundant-bang-patterns -Wredundant-strictness-flags+    if impl (ghc >= 9.8)+      ghc-options: -Wincomplete-export-warnings -Wmissing-poly-kind-signatures -Wterm-variable-capture+    if impl (ghc >= 9.10)+      ghc-options: -Wdefaulted-exception-context+  else+    ghc-options: -threaded -rtsopts -with-rtsopts=-N
+ henforcer.toml view
@@ -0,0 +1,10 @@+[forAnyModule]+allowedOpenUnaliasedImports = 1+moduleHeaderCopyrightMustExistNonEmpty = true+moduleHeaderDescriptionMustExistNonEmpty = false+moduleHeaderLicenseMustExistNonEmpty = true+moduleHeaderMaintainerMustExistNonEmpty = false+[[forPatternModules]]+pattern = "Paths_*"+[forPatternModules.rulesToIgnore]+all = true
src/Network/FTP/Client.hs view
@@ -1,811 +1,1173 @@-{-|-Module      : Network.FTP.Client-Description : Transfer files over FTP and FTPS-License     : Public Domain-Stability   : experimental-Portability : POSIX--}-module Network.FTP.Client (-    -- * Main Entrypoints-    withFTP,-    withFTPS,-    -- * Control Commands-    login,-    pasv,-    rename,-    dele,-    cwd,-    size,-    mkd,-    rmd,-    pwd,-    quit,-    -- * Data Commands-    nlst,-    retr,-    list,-    stor,-    mlsd,-    mlst,-    -- * Types-    FTPCommand(..),-    FTPResponse(..),-    FTPMessage(..),-    ResponseStatus(..),-    MlsxResponse(..),-    RTypeCode(..),-    PortActivity(..),-    ProtType(..),-    Security(..),-    Handle(..),-    -- * Exceptions-    FTPException(..),-    -- * Handle Implementations-    sIOHandleImpl,-    tlsHandleImpl,-    -- * Lower Level Functions-    sendCommand,-    sendCommandS,-    recvAll,-    sendAll,-    sendAllS,-    getLineResp,-    getResponse,-    getResponseS,-    sendCommandLine,-    createSendDataCommand,-    createTLSSendDataCommand,-    parseMlsxLine-) where--import Data.Default.Class (def)-import qualified Data.ByteString.Char8 as C-import qualified Data.ByteString as B-import Data.ByteString (ByteString)-import Data.List-import Data.Attoparsec.ByteString.Char8-import qualified Network.Socket as S-import qualified System.IO as SIO-import Data.Monoid ((<>))-import Control.Exception-import Control.Monad.Catch (MonadCatch, MonadMask)-import qualified Control.Monad.Catch as M-import Control.Monad-import Control.Monad.IO.Class-import Data.Bits-import Network.Connection-import Data.ByteString.Lazy.Internal (defaultChunkSize)-import Data.Functor ((<$>))-import Control.Applicative ((<*>))-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Control.Arrow-import Data.Typeable--debugging :: Bool-debugging = False--debugPrint :: (Show a, MonadIO m) => a -> m ()-debugPrint s = when debugging (liftIO $ print s)--debugResponse :: (Show a, MonadIO m) => a -> m ()-debugResponse s = debugPrint $ "Recieved: " <> show s--data Security = Clear | TLS---- | Can send and recieve a 'Data.ByteString.ByteString'.-data Handle = Handle-    { send :: ByteString -> IO ()-    , sendLine :: ByteString -> IO ()-    , recv :: Int -> IO ByteString-    , recvLine :: IO ByteString-    , security :: Security-    }--data FTPMessage = SingleLine ByteString | MultiLine [ByteString]-    deriving Eq--instance Show FTPMessage where-    show (SingleLine message) = C.unpack message-    show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages---- | Response from an FTP command. ex "200 Welcome!"-data FTPResponse = FTPResponse {-    frStatus :: ResponseStatus, -- ^ Interpretation of the first digit of an FTP response code-    frCode :: Int, -- ^ The three digit response code-    frMessage :: FTPMessage -- ^ Text of the response-} deriving Eq--instance Show FTPResponse where-    show fr = show (frCode fr) <> " " <> show (frMessage fr)---- | First digit of an FTP response-data ResponseStatus-    = Wait -- ^ 1-    | Success -- ^ 2-    | Continue -- ^ 3-    | FailureRetry -- ^ 4-    | Failure -- ^ 5-    deriving (Show, Eq)--data FTPException-    = FailureRetryException FTPResponse-    | FailureException FTPResponse-    | UnsuccessfulException FTPResponse-    | BogusResponseFormatException FTPResponse-    | BadProtocolResponseException ByteString-    deriving (Show, Typeable)--instance Exception FTPException--responseStatus :: ByteString -> ResponseStatus-responseStatus cbs =-    case C.uncons cbs of-        Just ('1', _) -> Wait-        Just ('2', _) -> Success-        Just ('3', _) -> Continue-        Just ('4', _) -> FailureRetry-        Just ('5', _) -> Failure-        _ -> throw $ BadProtocolResponseException cbs--data RTypeCode = TA | TI--serialzeRTypeCode :: RTypeCode -> String-serialzeRTypeCode TA = "A"-serialzeRTypeCode TI = "I"--data PortActivity = Active | Passive--data ProtType = P | C---- | Commands according to the FTP specification-data FTPCommand-    = User String-    | Pass String-    | Acct String-    | RType RTypeCode-    | Retr String-    | Nlst [String]-    | Port S.HostAddress S.PortNumber-    | Stor String-    | List [String]-    | Rnfr String-    | Rnto String-    | Dele String-    | Size String-    | Mkd String-    | Rmd String-    | Pbsz Int-    | Prot ProtType-    | Mlsd String-    | Mlst String-    | Cwd String-    | Cdup-    | Ccc-    | Auth-    | Pwd-    | Abor-    | Pasv-    | Quit--instance Show FTPCommand where-    show = serializeCommand--formatPort :: S.HostAddress -> S.PortNumber -> String-formatPort ha pn =-    let (w1, w2, w3, w4) = S.hostAddressToTuple ha-        hn = show <$> [w1, w2, w3, w4]-        portParts = show <$> [pn `quot` 256, pn `mod` 256]-    in intercalate "," (hn <> portParts)--serializeCommand :: FTPCommand -> String-serializeCommand (User user)  = "USER " <> user-serializeCommand (Pass pass)  = "PASS " <> pass-serializeCommand (Acct acct)  = "ACCT " <> acct-serializeCommand (RType rt)   = "TYPE " <> serialzeRTypeCode rt-serializeCommand (Retr file)  = "RETR " <> file-serializeCommand (Nlst [])    = "NLST"-serializeCommand (Nlst args)  = "NLST " <> unwords args-serializeCommand (Port ha pn) = "PORT " <> formatPort ha pn-serializeCommand (Stor loc)   = "STOR " <> loc-serializeCommand (List [])    = "LIST"-serializeCommand (List args)  = "LIST " <> unwords args-serializeCommand (Rnfr from)  = "RNFR " <> from-serializeCommand (Rnto to)    = "RNTO " <> to-serializeCommand (Dele file)  = "DELE " <> file-serializeCommand (Size file)  = "SIZE " <> file-serializeCommand (Mkd dir)    = "MKD " <> dir-serializeCommand (Rmd dir)    = "RMD " <> dir-serializeCommand (Pbsz buf)   = "PBSZ " <> show buf-serializeCommand (Prot P)     = "PROT P"-serializeCommand (Prot C)     = "PROT C"-serializeCommand (Mlsd path)  = "MLSD " <> path-serializeCommand (Mlst path)  = "MLST " <> path-serializeCommand (Cwd dir)    = "CWD " <> dir-serializeCommand Cdup         = "CDUP"-serializeCommand Ccc          = "CCC"-serializeCommand Auth         = "AUTH TLS"-serializeCommand Pwd          = "PWD"-serializeCommand Abor         = "ABOR"-serializeCommand Pasv         = "PASV"-serializeCommand Quit         = "QUIT"--stripCLRF :: ByteString -> ByteString-stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n')---- | Get a line from the server-getLineResp :: Handle -> IO ByteString-getLineResp h = stripCLRF <$> recvLine h---- | Get a full response from the server--- Used in 'sendCommand'-getResponse :: MonadIO m => Handle -> m FTPResponse-getResponse h = do-    line <- liftIO $ getLineResp h-    let (code, rest) = C.splitAt 3 line-    message <- if C.head rest == '-'-        then MultiLine <$> loopMultiLine h code [line]-        else return $ SingleLine line-    let codeDroppedMessage = case message of-            SingleLine message -> SingleLine $ C.drop 4 message-            MultiLine [] -> MultiLine []-            MultiLine (message:messages) ->-                MultiLine $ C.drop 4 message : messages-    let response = FTPResponse-            (responseStatus code)-            (read $ C.unpack code)-            codeDroppedMessage-    case frStatus response of-        FailureRetry -> liftIO $ throwIO $ FailureRetryException response-        Failure -> liftIO $ throwIO $ FailureException response-        _ -> return response--loopMultiLine-    :: MonadIO m-    => Handle-    -> ByteString-    -> [ByteString]-    -> m [ByteString]-loopMultiLine h code lines = do-    nextLine <- liftIO $ getLineResp h-    let newLines = lines <> [C.dropWhile (== ' ') nextLine]-        nextCode = C.take 3 nextLine-    if nextCode == code-        then return newLines-        else loopMultiLine h code newLines--ensureSuccess :: MonadIO m => FTPResponse -> m FTPResponse-ensureSuccess resp =-    case frStatus resp of-        Success -> return resp-        _ -> liftIO $ throwIO $ UnsuccessfulException resp--getResponseS :: MonadIO m => Handle -> m FTPResponse-getResponseS = ensureSuccess <=< getResponse--sendCommandLine :: MonadIO m => Handle -> ByteString -> m ()-sendCommandLine h = liftIO . send h . (<> "\r\n")---- | Send a command to the server and get a response back.--- Some commands use a data 'Handle', and their data is not returned here.-sendCommand :: MonadIO m => Handle -> FTPCommand -> m FTPResponse-sendCommand h fc = do-    let command = serializeCommand fc-    debugPrint $ "Sending: " <> command-    sendCommandLine h $ C.pack command-    resp <- getResponse h-    debugResponse resp-    return resp--sendCommandS :: MonadIO m => Handle -> FTPCommand -> m FTPResponse-sendCommandS h fc = sendCommand h fc >>= ensureSuccess---- | Equvalent to------ > mapM . sendCommand-sendAll :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]-sendAll = mapM . sendCommand---- | Equvalent to------ > mapM . sendCommandS-sendAllS :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]-sendAllS = mapM . sendCommandS---- Control connection--createSocket-    :: MonadIO m-    => Maybe String-    -> Int-    -> S.AddrInfo-    -> m (S.Socket, S.AddrInfo)-createSocket host portNum hints = do-    addr <- liftIO $ do-      a:_ <- S.getAddrInfo (Just hints) host (Just $ show portNum)-      return a-    debugPrint $ "Addr: " <> show addr-    sock <- liftIO $ S.socket-        (S.addrFamily addr)-        (S.addrSocketType addr)-        (S.addrProtocol addr)-    return (sock, addr)--withSocketPassive-    :: (MonadIO m, MonadMask m)-    => String-    -> Int-    -> (S.Socket -> m a)-    -> m a-withSocketPassive host portNum f = do-    let hints = S.defaultHints {-        S.addrSocketType = S.Stream-    }-    M.bracketOnError-        (createSocket (Just host) portNum hints)-        (liftIO . S.close . fst)-        (\(sock, addr) -> do-            debugPrint "Connecting"-            liftIO $ S.connect sock (S.addrAddress addr)-            debugPrint "Connected"-            f sock-        )--withSocketActive :: (MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a-withSocketActive f = do-    let hints = S.defaultHints {-        S.addrSocketType = S.Stream,-        S.addrFlags = [S.AI_PASSIVE]-    }-    M.bracketOnError-        (createSocket Nothing 0 hints)-        (liftIO . S.close . fst)-        (\(sock, addr) -> do-            debugPrint "Binding"-            liftIO $ S.bind sock (S.addrAddress addr)-            liftIO $ S.listen sock 1-            debugPrint "Listening"-            f sock-        )--createSIOHandle :: (MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle-createSIOHandle host portNum = withSocketPassive host portNum-    $ liftIO . flip S.socketToHandle SIO.ReadWriteMode--sIOHandleImpl :: SIO.Handle -> Handle-sIOHandleImpl h = Handle-    { send = C.hPut h-    , sendLine = C.hPutStrLn h-    , recv = C.hGetSome h-    , recvLine = C.hGetLine h-    , security = Clear-    }--withSIOHandle-    :: (MonadIO m, MonadMask m)-    => String-    -> Int-    -> (Handle -> m a)-    -> m a-withSIOHandle host portNum f = M.bracket-    (liftIO $ createSIOHandle host portNum)-    (liftIO . SIO.hClose)-    (f . sIOHandleImpl)---- | Takes a host name and port. A handle for interacting with the server--- will be returned in a callback.------ @--- withFTP "ftp.server.com" 21 $ \h welcome -> do---     print welcome---     login h "username" "password"---     print =<< nlst h []--- @-withFTP-    :: (MonadIO m, MonadMask m)-    => String-    -> Int-    -> (Handle -> FTPResponse -> m a)-    -> m a-withFTP host portNum f = withSIOHandle host portNum $ \h -> do-    resp <- getResponse h-    f h resp---- Data connection--withDataSocketPasv-    :: (MonadIO m, MonadMask m)-    => Handle-    -> (S.Socket -> m a)-    -> m a-withDataSocketPasv h f = do-    (host, portNum) <- pasv h-    debugPrint $ "Host: " <> host-    debugPrint $ "Port: " <> show portNum-    withSocketPassive host portNum f--withDataSocketActive-    :: (MonadIO m, MonadMask m)-    => Handle-    -> (S.Socket -> m a)-    -> m a-withDataSocketActive h f = withSocketActive $ \socket -> do-    (sPort, sHost) <- liftIO $ do-      (S.SockAddrInet p h) <- S.getSocketName socket-      return (p,h)-    port h sHost sPort-    f socket---- | Open a socket that can be used for data transfers-withDataSocket-    :: (MonadIO m, MonadMask m)-    => PortActivity-    -> Handle-    -> (S.Socket -> m a)-    -> m a-withDataSocket Active  = withDataSocketActive-withDataSocket Passive = withDataSocketPasv--acceptData :: MonadIO m => PortActivity -> S.Socket -> m S.Socket-acceptData Passive = return-acceptData Active = return . fst <=< liftIO . S.accept---- Response to data commands should be 150 but apparently--- some servers will respond with 200 before 150 so just ignore it-ensureSucessfulData :: MonadIO m => Handle -> FTPResponse -> m ()-ensureSucessfulData h resp = do-    resp' <- case frStatus resp of-        Success -> do-            newResp <- getResponse h-            debugResponse newResp-            return newResp-        _ -> return resp-    liftIO $ when (frStatus resp' /= Wait)-        $ throwIO $ UnsuccessfulException resp---- | Send setup commands to the server and--- create a data 'System.IO.Handle'-createSendDataCommand-    :: (MonadIO m, MonadMask m)-    => Handle-    -> PortActivity-    -> FTPCommand-    -> m SIO.Handle-createSendDataCommand h pa cmd = withDataSocket pa h $ \socket -> do-    resp <- sendCommand h cmd-    ensureSucessfulData h resp-    acceptedSock <- acceptData pa socket-    liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode---- | Provides a data 'Handle' in a callback for a command-withDataCommand-    :: (MonadIO m, MonadMask m)-    => Handle-    -> PortActivity-    -> RTypeCode-    -> FTPCommand-    -> (Handle -> m a)-    -> m a-withDataCommand ch pa code cmd f = do-    sendCommandS ch $ RType code-    x <- M.bracket-        (createSendDataCommand ch pa cmd)-        (liftIO . SIO.hClose)-        (f . sIOHandleImpl)-    resp <- getResponse ch-    debugResponse resp-    return x---- | Recieve data and interpret it linewise-getAllLineResp :: (MonadIO m, MonadCatch m) => Handle -> m ByteString-getAllLineResp h = getAllLineResp' h []-    where-        getAllLineResp' h ret = ( do-            line <- liftIO $ getLineResp h-            getAllLineResp' h (ret <> [line]))-                `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret)---- | Recieve all data and return it as a 'Data.ByteString.ByteString'-recvAll :: (MonadIO m, MonadCatch m) => Handle -> m ByteString-recvAll h = recvAll' ""-    where-        recvAll' bs = ( do-            chunk <- liftIO $ recv h defaultChunkSize-            if C.null chunk-               then return bs-               else recvAll' $ bs <> chunk-            ) `M.catchIOError` (\_ -> return bs)---- TLS connection--connectTLS :: MonadIO m => SIO.Handle -> String -> Int -> m Connection-connectTLS h host portNum = do-    context <- liftIO initConnectionContext-    let tlsSettings = def-            { settingDisableCertificateValidation = True-            }-        connectionParams = ConnectionParams-            { connectionHostname = host-            , connectionPort = toEnum . fromEnum $ portNum-            , connectionUseSecure = Just tlsSettings-            , connectionUseSocks = Nothing-            }-    liftIO $ connectFromHandle context h connectionParams--createTLSConnection-    :: (MonadIO m, MonadMask m)-    => String-    -> Int-    -> m (FTPResponse, Connection)-createTLSConnection host portNum = do-    h <- createSIOHandle host portNum-    let insecureH = sIOHandleImpl h-    resp <- getResponse insecureH-    sendCommand insecureH Auth-    conn <- connectTLS h host portNum-    return (resp, conn)--tlsHandleImpl :: Connection -> Handle-tlsHandleImpl c = Handle-    { send = connectionPut c-    , sendLine = connectionPut c . (<> "\n")-    , recv = connectionGet c-    , recvLine = connectionGetLine maxBound c-    , security = TLS-    }--withTLSHandle-    :: (MonadMask m, MonadIO m)-    => String-    -> Int-    -> (Handle -> FTPResponse -> m a)-    -> m a-withTLSHandle host portNum f = M.bracket-    (createTLSConnection host portNum)-    (liftIO . connectionClose . snd)-    (\(resp, conn) -> f (tlsHandleImpl conn) resp)---- | Takes a host name and port. A handle for interacting with the server--- will be returned in a callback. The commands will be protected with TLS.------ @--- withFTPS "ftps.server.com" 21 $ \h welcome -> do---     print welcome---     login h "username" "password"---     print =<< nlst h []--- @-withFTPS-    :: (MonadMask m, MonadIO m)-    => String-    -> Int-    -> (Handle -> FTPResponse -> m a)-    -> m a-withFTPS = withTLSHandle---- TLS data connection---- | Send setup commands to the server and--- create a data TLS connection-createTLSSendDataCommand-    :: (MonadIO m, MonadMask m)-    => Handle-    -> PortActivity-    -> FTPCommand-    -> m Connection-createTLSSendDataCommand ch pa cmd = do-    sendAllS ch [Pbsz 0, Prot P]-    withDataSocket pa ch $ \socket -> do-        resp <- sendCommand ch cmd-        ensureSucessfulData ch resp-        acceptedSock <- acceptData pa socket-        (sPort, sHost) <- liftIO $ do-          (S.SockAddrInet p h) <- S.getSocketName acceptedSock-          return (p, h)-        let (h1, h2, h3, h4) = S.hostAddressToTuple sHost-            hostName = intercalate "." $ show . fromEnum <$> [h1, h2, h3, h4]-        h <- liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode-        liftIO $ connectTLS h hostName (fromEnum sPort)--withTLSDataCommand-    :: (MonadIO m, MonadMask m)-    => Handle-    -> PortActivity-    -> RTypeCode-    -> FTPCommand-    -> (Handle -> m a)-    -> m a-withTLSDataCommand ch pa code cmd f = do-    sendCommandS ch $ RType code-    x <- M.bracket-        (createTLSSendDataCommand ch pa cmd)-        (liftIO . connectionClose)-        (f . tlsHandleImpl)-    resp <- getResponse ch-    debugPrint $ "Recieved: " <> show resp-    return x--parseResponse :: MonadIO m => FTPResponse -> Parser a -> m a-parseResponse resp p =-    let parsableMessage = case frMessage resp of-            SingleLine message -> message-            MultiLine messages -> C.intercalate "\n" messages-    in case parseOnly p parsableMessage of-        Right x -> return x-        Left _ -> liftIO $ throwIO-            $ BadProtocolResponseException parsableMessage--ensureCode :: MonadIO m => FTPResponse -> Int -> m ()-ensureCode resp code =-    liftIO $ when (frCode resp /= code)-        $ liftIO $ throwIO $ UnsuccessfulException resp--parse227 :: Parser (String, Int)-parse227 = do-    skipWhile (/= '(') *> char '('-    [h1,h2,h3,h4,p1,p2] <- many1 digit `sepBy` char ','-    let host = intercalate "." [h1,h2,h3,h4]-        highBits = read p1-        lowBits = read p2-        portNum = (highBits `shift` 8) + lowBits-    return (host, portNum)--parse257 :: Parser String-parse257 = do-    char '"'-    C.unpack <$> takeTill (== '"')---- Control commands--login :: MonadIO m => Handle -> String -> String -> m FTPResponse-login h user pass = do-    resp <- last <$> sendAll h [User user, Pass pass]-    ensureSuccess resp--pasv :: MonadIO m => Handle -> m (String, Int)-pasv h = do-    resp <- sendCommandS h Pasv-    ensureCode resp 227-    parseResponse resp parse227--port :: MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse-port h ha pn = sendCommandS h (Port ha pn)--acct :: MonadIO m => Handle -> String -> m FTPResponse-acct h pass = sendCommandS h (Acct pass)--rename :: MonadIO m => Handle -> String -> String -> m FTPResponse-rename h from to = do-    res <- sendCommand h (Rnfr from)-    case frStatus res of-        Continue -> sendCommandS h (Rnto to)-        _ -> return res--dele :: MonadIO m => Handle -> String -> m FTPResponse-dele h file = sendCommandS h (Dele file)--cwd :: MonadIO m => Handle -> String -> m FTPResponse-cwd h dir =-    sendCommandS h $ if dir == ".."-        then Cdup-        else Cwd dir--size :: MonadIO m => Handle -> String -> m Int-size h file = do-    resp <- sendCommandS h (Size file)-    ensureCode resp 213-    return $ case frMessage resp of-        SingleLine message -> read . C.unpack $ message-        MultiLine _ -> 0--mkd :: MonadIO m => Handle -> String -> m String-mkd h dir = do-    resp <- sendCommandS h (Mkd dir)-    ensureCode resp 257-    parseResponse resp parse257--rmd :: MonadIO m => Handle -> String -> m FTPResponse-rmd h dir = sendCommandS h (Rmd dir)--pwd :: MonadIO m => Handle -> m String-pwd h = do-    resp <- sendCommandS h Pwd-    ensureCode resp 257-    parseResponse resp parse257--quit :: MonadIO m => Handle -> m FTPResponse-quit h = sendCommandS h Quit--mlst :: (MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse-mlst h path = do-    resp <- sendCommandS h (Mlst path)-    case frMessage resp of-        SingleLine message -> return $ parseMlsxLine message-        MultiLine messages -> if length messages >= 2-            then return $ parseMlsxLine $ messages !! 1-            else liftIO $ throwIO $ BogusResponseFormatException resp---- TLS commands--pbsz :: MonadIO m => Handle -> Int -> m FTPResponse-pbsz h = sendCommandS h . Pbsz--prot :: MonadIO m => Handle -> ProtType -> m FTPResponse-prot h = sendCommandS h . Prot--ccc :: MonadIO m => Handle -> m FTPResponse-ccc h = sendCommandS h Ccc--auth :: MonadIO m => Handle -> m FTPResponse-auth h = sendCommandS h Auth---- Data commands--sendType :: MonadIO m => RTypeCode -> ByteString -> Handle -> m ()-sendType TA dat h = mapM_ (sendCommandLine h) $ C.split '\n' dat-sendType TI dat h = liftIO $ send h dat--withDataCommandSecurity-    :: (MonadIO m, MonadMask m)-    => Handle-    -> PortActivity-    -> RTypeCode-    -> FTPCommand-    -> (Handle -> m a)-    -> m a-withDataCommandSecurity h =-    case security h of-        Clear -> withDataCommand h-        TLS -> withTLSDataCommand h--nlst :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString-nlst h args = withDataCommandSecurity h Passive TA (Nlst args) getAllLineResp--retr :: (MonadIO m, MonadMask m) => Handle -> String -> m ByteString-retr h path = withDataCommandSecurity h Passive TI (Retr path) recvAll--list :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString-list h args = withDataCommandSecurity h Passive TA (List args) getAllLineResp--stor-    :: (MonadIO m, MonadMask m)-    => Handle-    -> String-    -> B.ByteString-    -> RTypeCode-    -> m ()-stor h loc dat rtype =-    withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat--data MlsxResponse = MlsxResponse {-    mrFilename :: String,-    mrFacts :: Map String String-} deriving (Show)--splitApart :: Char -> ByteString -> (ByteString, ByteString)-splitApart on s =-    let (x0, x1) = C.break (== on) s-    in (x0, C.drop 1 x1)--parseMlsxLine :: ByteString -> MlsxResponse-parseMlsxLine line =-    let (factLine, filename) = splitApart ' ' line-        bFacts = splitApart '=' <$> C.split ';' factLine-        facts-            = Map.fromList-            $ filter (not . null . fst)-            $ join (***) C.unpack <$> bFacts-    in MlsxResponse (C.unpack filename) facts--getMlsxResponse :: (MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse]-getMlsxResponse h = getMlsxResponse' h []-    where-        getMlsxResponse' h ret = ( do-            line <- liftIO $ getLineResp h-            getMlsxResponse' h $-                if C.null line-                    then ret-                    else parseMlsxLine line : ret-            ) `M.catchIOError` (\_ -> return ret)--mlsd :: (MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse]+{- |+Module      : Network.FTP.Client+Description : Transfer files over FTP and FTPS+Copyright   : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026+License     : Public Domain+Stability   : experimental+Portability : POSIX+-}+module Network.FTP.Client+  ( -- * Main Entrypoints+    withFTP+  , withFTPS+  , withFTPSSettings++    -- * Control Commands+  , login+  , pasv+  , rename+  , dele+  , cwd+  , size+  , acct+  , mkd+  , rmd+  , pwd+  , quit++    -- * Data Commands+  , nlst+  , retr+  , list+  , stor+  , mlsd+  , mlst++    -- * Types+  , FTPCommand (..)+  , FTPResponse (..)+  , FTPMessage (..)+  , ResponseStatus (..)+  , MlsxResponse (..)+  , RTypeCode (..)+  , PortActivity (..)+  , ProtType (..)+  , Security (..)+  , TLSContext (..)+  , Handle (..)++    -- * TLS Commands+  , pbsz+  , prot++    -- * Exceptions+  , FTPException (..)++    -- * System Handle Creation+  , createSIOHandle+  , createTLSConnection+  , connectTLS++    -- * Handle Implementations+  , sIOHandleImpl+  , tlsHandleImpl++    -- * Lower Level Functions+  , sendCommand+  , sendCommandS+  , recvAll+  , sendAll+  , sendAllS+  , getLineResp+  , getAllLineResp+  , getLineRespMaybe+  , maxReplyLineLength+  , toNetworkAscii+  , requireTLSContext+  , getResponse+  , getResponseS+  , sendCommandLine+  , createSendDataCommand+  , createTLSSendDataCommand+  , PendingCompletion+  , newPendingCompletion+  , drainPendingCompletion+  , parseMlsxLine+  ) where++import Control.Arrow ((***))+import qualified Control.Exception as Exception+import Control.Monad ((<=<))+import qualified Control.Monad as Monad+import Control.Monad.Catch (MonadCatch, MonadMask)+import qualified Control.Monad.Catch as M+import qualified Control.Monad.IO.Class as MIO+import qualified Data.Attoparsec.ByteString.Char8 as AC+import qualified Data.Bits as Bits+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as C+import Data.ByteString.Lazy.Internal (defaultChunkSize)+import Data.Default.Class (def)+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Data.List (intercalate)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import qualified Network.Connection as Connection+import qualified Network.Socket as S+import qualified System.IO as SIO+import System.IO.Error (eofErrorType, isEOFError, mkIOError)++debugging :: Bool+debugging = False++debugPrint :: (Show a, MIO.MonadIO m) => a -> m ()+debugPrint s = Monad.when debugging (MIO.liftIO $ print s)++debugResponse :: (Show a, MIO.MonadIO m) => a -> m ()+debugResponse s = debugPrint $ "Recieved: " <> show s++{- | What a data connection needs in order to be protected the same way the+control connection is. The host is the one the control connection was opened+to, which is what the server's certificate is issued for -- deriving it from+the data socket instead names the local end and can never validate.+-}+data TLSContext = TLSContext+  { tlsContextSettings :: Connection.TLSSettings+  , tlsContextHost :: String+  , tlsContextPort :: Int+  }++-- Only TLS settings are threaded through the connection helpers today. If the+-- rest of the hardening lands -- ignoring the address in a PASV reply, a+-- timeout on the active-mode accept, a bound on data-channel line length --+-- those belong together with these as fields of one options record passed to+-- the with* functions, rather than as further positional parameters. That was+-- @qxjit's suggestion on https://github.com/flipstone/ftp-client/pull/1.++data Security = Clear | TLS TLSContext++-- | Can send and recieve a 'Data.ByteString.ByteString'.+data Handle = Handle+  { send :: ByteString -> IO ()+  , sendLine :: ByteString -> IO ()+  , recv :: Int -> IO ByteString+  , recvLine :: IO ByteString+  , security :: Security+  }++data FTPMessage = SingleLine ByteString | MultiLine [ByteString]+  deriving (Eq)++instance Show FTPMessage where+  show (SingleLine message) = C.unpack message+  show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages++-- | Response from an FTP command. ex "200 Welcome!"+data FTPResponse = FTPResponse+  { frStatus :: ResponseStatus+  -- ^ Interpretation of the first digit of an FTP response code+  , frCode :: Int+  -- ^ The three digit response code+  , frMessage :: FTPMessage+  -- ^ Text of the response+  }+  deriving (Eq)++instance Show FTPResponse where+  show fr = show (frCode fr) <> " " <> show (frMessage fr)++-- | First digit of an FTP response+data ResponseStatus+  = -- | 1+    Wait+  | -- | 2+    Success+  | -- | 3+    Continue+  | -- | 4+    FailureRetry+  | -- | 5+    Failure+  deriving (Show, Eq)++data FTPException+  = FailureRetryException FTPResponse+  | FailureException FTPResponse+  | UnsuccessfulException FTPResponse+  | BogusResponseFormatException FTPResponse+  | BadProtocolResponseException ByteString+  deriving (Show)++instance Exception.Exception FTPException++responseStatus :: ByteString -> ResponseStatus+responseStatus cbs =+  case C.uncons cbs of+    Just ('1', _) -> Wait+    Just ('2', _) -> Success+    Just ('3', _) -> Continue+    Just ('4', _) -> FailureRetry+    Just ('5', _) -> Failure+    _ -> Exception.throw $ BadProtocolResponseException cbs++data RTypeCode = TA | TI++serialzeRTypeCode :: RTypeCode -> String+serialzeRTypeCode TA = "A"+serialzeRTypeCode TI = "I"++data PortActivity = Active | Passive++data ProtType = P | C++-- | Commands according to the FTP specification+data FTPCommand+  = User String+  | Pass String+  | Acct String+  | RType RTypeCode+  | Retr String+  | Nlst [String]+  | Port S.HostAddress S.PortNumber+  | Stor String+  | List [String]+  | Rnfr String+  | Rnto String+  | Dele String+  | Size String+  | Mkd String+  | Rmd String+  | Pbsz Int+  | Prot ProtType+  | Mlsd String+  | Mlst String+  | Cwd String+  | Cdup+  | Ccc+  | Auth+  | Pwd+  | Abor+  | Pasv+  | Quit++instance Show FTPCommand where+  show = serializeCommand++formatPort :: S.HostAddress -> S.PortNumber -> String+formatPort ha pn =+  let+    (w1, w2, w3, w4) = S.hostAddressToTuple ha+    hn = show <$> [w1, w2, w3, w4]+    portParts = show <$> [pn `quot` 256, pn `mod` 256]+  in+    intercalate "," (hn <> portParts)++serializeCommand :: FTPCommand -> String+serializeCommand (User user) = "USER " <> user+serializeCommand (Pass pass) = "PASS " <> pass+serializeCommand (Acct account) = "ACCT " <> account+serializeCommand (RType rt) = "TYPE " <> serialzeRTypeCode rt+serializeCommand (Retr file) = "RETR " <> file+serializeCommand (Nlst []) = "NLST"+serializeCommand (Nlst args) = "NLST " <> unwords args+serializeCommand (Port ha pn) = "PORT " <> formatPort ha pn+serializeCommand (Stor loc) = "STOR " <> loc+serializeCommand (List []) = "LIST"+serializeCommand (List args) = "LIST " <> unwords args+serializeCommand (Rnfr from) = "RNFR " <> from+serializeCommand (Rnto to) = "RNTO " <> to+serializeCommand (Dele file) = "DELE " <> file+serializeCommand (Size file) = "SIZE " <> file+serializeCommand (Mkd dir) = "MKD " <> dir+serializeCommand (Rmd dir) = "RMD " <> dir+serializeCommand (Pbsz buf) = "PBSZ " <> show buf+serializeCommand (Prot P) = "PROT P"+serializeCommand (Prot C) = "PROT C"+serializeCommand (Mlsd path) = "MLSD " <> path+serializeCommand (Mlst path) = "MLST " <> path+serializeCommand (Cwd dir) = "CWD " <> dir+serializeCommand Cdup = "CDUP"+serializeCommand Ccc = "CCC"+serializeCommand Auth = "AUTH TLS"+serializeCommand Pwd = "PWD"+serializeCommand Abor = "ABOR"+serializeCommand Pasv = "PASV"+serializeCommand Quit = "QUIT"++{- | Cap on one reply line. RFC 959 replies are short; this exists so a server+that never sends a newline cannot make us buffer without limit. It applies to+both control connections: 'Connection.connectionGetLine' takes it directly, and+'hGetLineBounded' applies it to a clear handle. Exceeding it raises+'Connection.LineTooLong', which is not an 'IOError' and so is not swallowed by+the end-of-input handling elsewhere in this module.+-}+maxReplyLineLength :: Int+maxReplyLineLength = 65536++{- | 'C.hGetLine' with a bound, for handles that 'Network.Connection' is not+managing. Reads a byte at a time rather than in chunks because 'recv' shares+the handle: anything taken past the newline would be stolen from the data the+caller asks for next. The handle is buffered, so this is not a syscall per+byte, and reply lines are short.++End of input is reported the way 'C.hGetLine' reports it, because+'getLineRespMaybe' relies on the distinction: throw when nothing has been read+yet, hand back the partial line when something has.+-}+hGetLineBounded :: Int -> SIO.Handle -> IO ByteString+hGetLineBounded limit h =+  let+    collect :: Int -> [ByteString] -> IO ByteString+    collect remaining acc+      | remaining <= 0 = Exception.throwIO Connection.LineTooLong+      | otherwise = do+          byte <- B.hGet h 1+          if B.null byte+            then+              if null acc+                then+                  Exception.throwIO $+                    mkIOError eofErrorType "hGetLineBounded" (Just h) Nothing+                else return $ B.concat (reverse acc)+            else+              if byte == C.singleton '\n'+                then return $ B.concat (reverse acc)+                else collect (remaining - 1) (byte : acc)+  in+    collect limit []++stripCLRF :: ByteString -> ByteString+stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n')++-- | Get a line from the server+getLineResp :: Handle -> IO ByteString+getLineResp h = stripCLRF <$> recvLine h++{- | Get a line from the server, returning 'Nothing' once the stream is+exhausted. A blank line and end of input are different things: 'recvLine'+signals end of input by throwing, and an empty 'ByteString' is a legitimate+line of reply text.+-}+getLineRespMaybe :: Handle -> IO (Maybe ByteString)+getLineRespMaybe h =+  (Just <$> getLineResp h) `M.catchIOError` \e ->+    if isEOFError e+      then return Nothing+      else ioError e++{- | Get a full response from the server+Used in 'sendCommand'+-}+getResponse :: MIO.MonadIO m => Handle -> m FTPResponse+getResponse h = do+  line <- MIO.liftIO $ getLineResp h+  let+    (code, rest) = C.splitAt 3 line+  -- A response must open with a three digit code. Checking that up front keeps+  -- the 'C.uncons' below and the 'read' further down from being partial.+  Monad.when (C.length code < 3 || not (C.all AC.isDigit code)) $+    MIO.liftIO $+      Exception.throwIO $+        BadProtocolResponseException line+  message <- case C.uncons rest of+    Just ('-', _) -> MultiLine <$> loopMultiLine h code [line]+    _ -> return $ SingleLine line+  let+    codeDroppedMessage = case message of+      SingleLine singleMessage -> SingleLine $ C.drop 4 singleMessage+      MultiLine [] -> MultiLine []+      MultiLine (firstMessage : messages) ->+        MultiLine $ C.drop 4 firstMessage : messages+  let+    response =+      FTPResponse+        (responseStatus code)+        (read $ C.unpack code)+        codeDroppedMessage+  case frStatus response of+    FailureRetry -> MIO.liftIO $ Exception.throwIO $ FailureRetryException response+    Failure -> MIO.liftIO $ Exception.throwIO $ FailureException response+    _ -> return response++loopMultiLine ::+  MIO.MonadIO m =>+  Handle ->+  ByteString ->+  [ByteString] ->+  m [ByteString]+loopMultiLine h code priorLines = do+  mNextLine <- MIO.liftIO $ getLineRespMaybe h+  case mNextLine of+    -- The server hung up before sending the terminating line. Stop rather+    -- than looping forever, but treat the reply as bad rather than+    -- returning it: what was collected is a fragment, and handing it back+    -- would turn a truncated reply into a well formed one. A cut off "220-"+    -- greeting would read as a successful 220 and let 'withFTP' carry on+    -- against a control connection that is already gone.+    --+    -- This is end of input, not a blank line. RFC 959 lets the intermediate+    -- lines of a multiline reply hold arbitrary text, blank lines included,+    -- so a blank line has to be kept and the loop has to continue past it.+    Nothing ->+      MIO.liftIO $+        Exception.throwIO $+          BadProtocolResponseException $+            C.intercalate "\n" priorLines+    Just nextLine -> do+      -- RFC 959 (https://datatracker.ietf.org/doc/html/rfc959#page-36) ends a+      -- multiline reply with the code followed by a space, and continues it+      -- with the code followed by a hyphen. The bare code is accepted too,+      -- for servers that omit the trailing space on an empty final line.+      let+        newLines = priorLines <> [C.dropWhile (== ' ') nextLine]+        isLastLine =+          nextLine == code+            || C.isPrefixOf (code <> " ") nextLine+      if isLastLine+        then return newLines+        else loopMultiLine h code newLines++ensureSuccess :: MIO.MonadIO m => FTPResponse -> m FTPResponse+ensureSuccess resp =+  case frStatus resp of+    Success -> return resp+    _ -> MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp++getResponseS :: MIO.MonadIO m => Handle -> m FTPResponse+getResponseS = ensureSuccess <=< getResponse++sendCommandLine :: MIO.MonadIO m => Handle -> ByteString -> m ()+sendCommandLine h = MIO.liftIO . send h . (<> "\r\n")++{- | Send a command to the server and get a response back.+Some commands use a data 'Handle', and their data is not returned here.+-}+sendCommand :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse+sendCommand h fc = do+  let+    command = serializeCommand fc+  debugPrint $ "Sending: " <> command+  sendCommandLine h $ C.pack command+  resp <- getResponse h+  debugResponse resp+  return resp++sendCommandS :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse+sendCommandS h fc = sendCommand h fc >>= ensureSuccess++{- | Equvalent to++> mapM . sendCommand+-}+sendAll :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]+sendAll = mapM . sendCommand++{- | Equvalent to++> mapM . sendCommandS+-}+sendAllS :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]+sendAllS = mapM . sendCommandS++-- Control connection++createSocket ::+  MIO.MonadIO m =>+  Maybe String ->+  Int ->+  S.AddrInfo ->+  m (S.Socket, S.AddrInfo)+createSocket host portNum hints = do+  addr <- MIO.liftIO $ do+    a : _ <- S.getAddrInfo (Just hints) host (Just $ show portNum)+    return a+  debugPrint $ "Addr: " <> show addr+  sock <-+    MIO.liftIO $+      S.socket+        (S.addrFamily addr)+        (S.addrSocketType addr)+        (S.addrProtocol addr)+  return (sock, addr)++withSocketPassive ::+  (MIO.MonadIO m, MonadMask m) =>+  String ->+  Int ->+  (S.Socket -> m a) ->+  m a+withSocketPassive host portNum f = do+  let+    hints =+      S.defaultHints+        { S.addrSocketType = S.Stream+        }+  -- bracketOnError, not bracket: on success this socket is handed to+  -- socketToHandle, which takes ownership of the descriptor, so closing it here+  -- as well would be wrong. Contrast withSocketActive above.+  M.bracketOnError+    (createSocket (Just host) portNum hints)+    (MIO.liftIO . S.close . fst)+    ( \(sock, addr) -> do+        debugPrint ("Connecting" :: String)+        MIO.liftIO $ S.connect sock (S.addrAddress addr)+        debugPrint ("Connected" :: String)+        f sock+    )++withSocketActive :: (MIO.MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a+withSocketActive f = do+  let+    hints =+      S.defaultHints+        { S.addrSocketType = S.Stream+        , S.addrFlags = [S.AI_PASSIVE]+        }+  -- bracket, not bracketOnError: in active mode acceptData returns a *new*+  -- socket and only that one is converted to a Handle, so ownership of this+  -- listening socket is never transferred and it must be closed on the success+  -- path too. The passive helper below is the opposite case and deliberately+  -- differs.+  M.bracket+    (createSocket Nothing 0 hints)+    (MIO.liftIO . S.close . fst)+    ( \(sock, addr) -> do+        debugPrint ("Binding" :: String)+        MIO.liftIO $ S.bind sock (S.addrAddress addr)+        MIO.liftIO $ S.listen sock 1+        debugPrint ("Listening" :: String)+        f sock+    )++createSIOHandle :: (MIO.MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle+createSIOHandle host portNum =+  withSocketPassive host portNum $+    MIO.liftIO . flip S.socketToHandle SIO.ReadWriteMode++sIOHandleImpl :: SIO.Handle -> Handle+sIOHandleImpl h =+  Handle+    { send = C.hPut h+    , -- RFC 959 TYPE A data uses CRLF, not a bare LF+      sendLine = \s -> C.hPut h (s <> "\r\n")+    , recv = C.hGetSome h+    , recvLine = hGetLineBounded maxReplyLineLength h+    , security = Clear+    }++withSIOHandle ::+  (MIO.MonadIO m, MonadMask m) =>+  String ->+  Int ->+  (Handle -> m a) ->+  m a+withSIOHandle host portNum f =+  M.bracket+    (MIO.liftIO $ createSIOHandle host portNum)+    (MIO.liftIO . SIO.hClose)+    (f . sIOHandleImpl)++{- | Takes a host name and port. A handle for interacting with the server+will be returned in a callback.++@+withFTP "ftp.server.com" 21 $ \h welcome -> do+    print welcome+    login h "username" "password"+    print =<< nlst h []+@+-}+withFTP ::+  (MIO.MonadIO m, MonadMask m) =>+  String ->+  Int ->+  (Handle -> FTPResponse -> m a) ->+  m a+withFTP host portNum f = withSIOHandle host portNum $ \h -> do+  resp <- getResponse h+  f h resp++-- Data connection++withDataSocketPasv ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  (S.Socket -> m a) ->+  m a+withDataSocketPasv h f = do+  (host, portNum) <- pasv h+  debugPrint $ "Host: " <> host+  debugPrint $ "Port: " <> show portNum+  withSocketPassive host portNum f++withDataSocketActive ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  (S.Socket -> m a) ->+  m a+withDataSocketActive h f = withSocketActive $ \socket -> do+  (sPort, sHost) <- MIO.liftIO $ do+    (S.SockAddrInet p hostAddr) <- S.getSocketName socket+    return (p, hostAddr)+  _ <- port h sHost sPort+  f socket++-- | Open a socket that can be used for data transfers+withDataSocket ::+  (MIO.MonadIO m, MonadMask m) =>+  PortActivity ->+  Handle ->+  (S.Socket -> m a) ->+  m a+withDataSocket Active = withDataSocketActive+withDataSocket Passive = withDataSocketPasv++acceptData :: MIO.MonadIO m => PortActivity -> S.Socket -> m S.Socket+acceptData Passive = return+acceptData Active = return . fst <=< MIO.liftIO . S.accept++-- Response to data commands should be 150 but apparently+-- some servers will respond with 200 before 150 so just ignore it+ensureSucessfulData :: MIO.MonadIO m => Handle -> FTPResponse -> m ()+ensureSucessfulData h resp = do+  resp' <- case frStatus resp of+    Success -> do+      newResp <- getResponse h+      debugResponse newResp+      return newResp+    _ -> return resp+  MIO.liftIO $+    Monad.when (frStatus resp' /= Wait) $+      Exception.throwIO $+        UnsuccessfulException resp++{- | Send setup commands to the server and+create a data 'System.IO.Handle'+-}+createSendDataCommand ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  PortActivity ->+  PendingCompletion ->+  FTPCommand ->+  m SIO.Handle+createSendDataCommand h pa pending cmd = withDataSocket pa h $ \socket -> do+  resp <- sendCommand h cmd+  ensureSucessfulData h resp+  -- Past this point the server has accepted the transfer, so a completion+  -- reply is owed to us however the rest of it goes.+  markCompletionPending pending+  acceptedSock <- acceptData pa socket+  MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode++-- | Provides a data 'Handle' in a callback for a command+withDataCommand ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  PortActivity ->+  RTypeCode ->+  FTPCommand ->+  (Handle -> m a) ->+  m a+withDataCommand ch pa code cmd f = do+  _ <- sendCommandS ch $ RType code+  pending <- newPendingCompletion+  x <-+    M.bracket+      (createSendDataCommand ch pa pending cmd)+      (MIO.liftIO . SIO.hClose)+      (f . sIOHandleImpl)+      `M.onException` drainPendingCompletion ch pending+  resp <- getResponse ch+  debugResponse resp+  return x++-- | Recieve data and interpret it linewise+getAllLineResp :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString+getAllLineResp h =+  let+    collect :: MIO.MonadIO n => [ByteString] -> n ByteString+    collect ret = do+      mLine <- MIO.liftIO $ getLineRespMaybe h+      case mLine of+        Nothing -> return $ C.intercalate (C.pack "\n") ret+        Just line -> collect (ret <> [line])+  in+    collect []++-- | Recieve all data and return it as a 'Data.ByteString.ByteString'+recvAll :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString+recvAll h =+  let+    collect :: (MIO.MonadIO n, MonadCatch n) => ByteString -> n ByteString+    collect bs =+      ( do+          -- No handler here on purpose. recv returns "" at end of data, so+          -- catching IOErrors would only turn a reset or timed-out connection+          -- into a short read that looks like a complete one.+          chunk <- MIO.liftIO $ recv h defaultChunkSize+          if C.null chunk+            then return bs+            else collect $ bs <> chunk+      )+  in+    collect ""++-- TLS connection++{- | Wrap an existing handle in TLS. The settings are supplied by the caller;+'def' validates the server's certificate chain and host name. Passing settings+with 'Connection.settingDisableCertificateValidation' set gives an encrypted+but unauthenticated connection, which any on-path attacker can read and rewrite.+-}+connectTLS ::+  MIO.MonadIO m =>+  Connection.TLSSettings ->+  SIO.Handle ->+  String ->+  Int ->+  m Connection.Connection+connectTLS settings h host portNum = do+  context <- MIO.liftIO Connection.initConnectionContext+  let+    connectionParams =+      Connection.ConnectionParams+        { Connection.connectionHostname = host+        , Connection.connectionPort = toEnum . fromEnum $ portNum+        , Connection.connectionUseSecure = Just settings+        , Connection.connectionUseSocks = Nothing+        }+  MIO.liftIO $ Connection.connectFromHandle context h connectionParams++createTLSConnection ::+  (MIO.MonadIO m, MonadMask m) =>+  Connection.TLSSettings ->+  String ->+  Int ->+  m (FTPResponse, Connection.Connection)+createTLSConnection settings host portNum =+  -- Without this the socket leaks whenever the greeting is a refusal, AUTH TLS+  -- is rejected, or the handshake fails. This is the acquire action of+  -- withTLSHandle's bracket, so its release would never run.+  M.bracketOnError+    (createSIOHandle host portNum)+    (MIO.liftIO . SIO.hClose)+    ( \h -> do+        let+          insecureH = sIOHandleImpl h+        resp <- getResponse insecureH+        _ <- sendCommandS insecureH Auth+        conn <- connectTLS settings h host portNum+        return (resp, conn)+    )++tlsHandleImpl :: TLSContext -> Connection.Connection -> Handle+tlsHandleImpl tlsContext c =+  Handle+    { send = Connection.connectionPut c+    , -- RFC 959 TYPE A data uses CRLF, not a bare LF+      sendLine = Connection.connectionPut c . (<> "\r\n")+    , recv = Connection.connectionGet c+    , recvLine = Connection.connectionGetLine maxReplyLineLength c+    , security = TLS tlsContext+    }++withTLSHandle ::+  (MonadMask m, MIO.MonadIO m) =>+  Connection.TLSSettings ->+  String ->+  Int ->+  (Handle -> FTPResponse -> m a) ->+  m a+withTLSHandle settings host portNum f =+  let+    tlsContext =+      TLSContext+        { tlsContextSettings = settings+        , tlsContextHost = host+        , tlsContextPort = portNum+        }+  in+    M.bracket+      (createTLSConnection settings host portNum)+      (MIO.liftIO . Connection.connectionClose . snd)+      (\(resp, conn) -> f (tlsHandleImpl tlsContext conn) resp)++{- | Takes a host name and port. A handle for interacting with the server+will be returned in a callback. The connection is protected with TLS and the+server's certificate chain and host name are verified, so a failure to validate+aborts the connection.++@+withFTPS "ftps.server.com" 21 $ \h welcome -> do+    print welcome+    login h "username" "password"+    print =<< nlst h []+@++Use 'withFTPSSettings' if you need to talk to a server whose certificate cannot+be validated.+-}+withFTPS ::+  (MonadMask m, MIO.MonadIO m) =>+  String ->+  Int ->+  (Handle -> FTPResponse -> m a) ->+  m a+withFTPS = withTLSHandle def++{- | As 'withFTPS', but with caller supplied TLS settings.++Setting 'Connection.settingDisableCertificateValidation' accepts any+certificate, including one an attacker generated. The connection is then+encrypted but not authenticated: anyone on the network path can read the+credentials sent by 'login' and alter transferred data. Only do this when you+have another way to establish that the peer is who it claims to be.++@+let insecure = 'def' { 'Connection.settingDisableCertificateValidation' = True }+withFTPSSettings insecure "ftps.server.com" 21 $ \h welcome -> do+    print welcome+@+-}+withFTPSSettings ::+  (MonadMask m, MIO.MonadIO m) =>+  Connection.TLSSettings ->+  String ->+  Int ->+  (Handle -> FTPResponse -> m a) ->+  m a+withFTPSSettings = withTLSHandle++-- TLS data connection++{- | The TLS details of a control connection, for reproducing them on a data+connection.+-}+requireTLSContext :: MIO.MonadIO m => Handle -> m TLSContext+requireTLSContext h =+  case security h of+    TLS ctx -> return ctx+    Clear ->+      MIO.liftIO . Exception.throwIO . BadProtocolResponseException $+        C.pack "cannot open a TLS data connection over a clear control connection"++{- | Read the reply that terminates a data transfer, discarding any failure.++Used on the paths where the transfer did not complete normally. The reply still+has to be taken off the control connection: left there it becomes the answer to+whichever command is sent next, and every reply after that belongs to the+previous command for the rest of the session. Failures are swallowed so this+cannot mask the exception that brought us here.+-}+drainDataResponse :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ()+drainDataResponse ch =+  Monad.void (getResponse ch) `M.catchAll` (\_ -> return ())++{- | Whether the server has accepted the preliminary reply to a transfer+command, and so whether a completion reply is on its way.++A transfer that is rejected outright -- @PBSZ@, @PROT@, @PASV@ or the transfer+command itself -- fails with its error reply already consumed by+'ensureSucessfulData', and nothing further is coming. Draining then waits for a+reply the server will never send, which blocks until it gives up on the+connection. Draining is only correct once this says a reply is pending.+-}+newtype PendingCompletion = PendingCompletion (IORef Bool)++newPendingCompletion :: MIO.MonadIO m => m PendingCompletion+newPendingCompletion =+  MIO.liftIO $ PendingCompletion <$> newIORef False++markCompletionPending :: MIO.MonadIO m => PendingCompletion -> m ()+markCompletionPending (PendingCompletion ref) =+  MIO.liftIO $ writeIORef ref True++{- | 'drainDataResponse', but only when a completion reply is actually pending.++This deliberately stays outside the bracket that owns the data connection. The+server sends the completion reply once that connection has closed, so draining+before the release action has run would block on a reply that is waiting on us.+-}+drainPendingCompletion ::+  (MIO.MonadIO m, MonadCatch m) =>+  Handle ->+  PendingCompletion ->+  m ()+drainPendingCompletion ch (PendingCompletion ref) = do+  pending <- MIO.liftIO $ readIORef ref+  Monad.when pending $ drainDataResponse ch++{- | Send setup commands to the server and+create a data TLS connection+-}+createTLSSendDataCommand ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  PortActivity ->+  PendingCompletion ->+  FTPCommand ->+  m Connection.Connection+createTLSSendDataCommand ch pa pending cmd = do+  tlsContext <- requireTLSContext ch+  _ <- sendAllS ch [Pbsz 0, Prot P]+  withDataSocket pa ch $ \socket -> do+    resp <- sendCommand ch cmd+    ensureSucessfulData ch resp+    -- Past this point the server has accepted the transfer, so a completion+    -- reply is owed to us however the rest of it goes -- a rejected+    -- certificate in the handshake below included.+    markCompletionPending pending+    acceptedSock <- acceptData pa socket+    -- socketToHandle invalidates the socket, so the enclosing bracketOnError's+    -- close becomes a no-op from here on; protect the handle separately or a+    -- failed handshake leaks the descriptor.+    M.bracketOnError+      (MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode)+      (MIO.liftIO . SIO.hClose)+      ( \h ->+          -- Authenticate against the host the control connection was opened to.+          -- getSocketName here would name the local end of the data socket,+          -- which no server certificate can ever match.+          MIO.liftIO $+            connectTLS+              (tlsContextSettings tlsContext)+              h+              (tlsContextHost tlsContext)+              (tlsContextPort tlsContext)+      )++withTLSDataCommand ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  PortActivity ->+  RTypeCode ->+  FTPCommand ->+  (Handle -> m a) ->+  m a+withTLSDataCommand ch pa code cmd f = do+  tlsContext <- requireTLSContext ch+  _ <- sendCommandS ch $ RType code+  pending <- newPendingCompletion+  x <-+    M.bracket+      (createTLSSendDataCommand ch pa pending cmd)+      (MIO.liftIO . Connection.connectionClose)+      (f . tlsHandleImpl tlsContext)+      `M.onException` drainPendingCompletion ch pending+  resp <- getResponse ch+  debugPrint $ "Recieved: " <> show resp+  return x++parseResponse :: MIO.MonadIO m => FTPResponse -> AC.Parser a -> m a+parseResponse resp p =+  let+    parsableMessage = case frMessage resp of+      SingleLine message -> message+      MultiLine messages -> C.intercalate "\n" messages+  in+    case AC.parseOnly p parsableMessage of+      Right x -> return x+      Left _ ->+        MIO.liftIO $+          Exception.throwIO $+            BadProtocolResponseException parsableMessage++ensureCode :: MIO.MonadIO m => FTPResponse -> Int -> m ()+ensureCode resp code =+  MIO.liftIO $+    Monad.when (frCode resp /= code) $+      MIO.liftIO $+        Exception.throwIO $+          UnsuccessfulException resp++parse227 :: AC.Parser (String, Int)+parse227 = do+  _ <- AC.skipWhile (/= '(') *> AC.char '('+  [h1, h2, h3, h4, p1, p2] <- AC.many1 AC.digit `AC.sepBy` AC.char ','+  let+    host = intercalate "." [h1, h2, h3, h4]+    highBits = read p1+    lowBits = read p2+    portNum = (highBits `Bits.shift` 8) + lowBits+  return (host, portNum)++parse257 :: AC.Parser String+parse257 = do+  _ <- AC.char '"'+  C.unpack <$> AC.takeTill (== '"')++-- Control commands++login :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse+login h user pass = do+  resp <- last <$> sendAll h [User user, Pass pass]+  ensureSuccess resp++pasv :: MIO.MonadIO m => Handle -> m (String, Int)+pasv h = do+  resp <- sendCommandS h Pasv+  ensureCode resp 227+  parseResponse resp parse227++port :: MIO.MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse+port h ha pn = sendCommandS h (Port ha pn)++acct :: MIO.MonadIO m => Handle -> String -> m FTPResponse+acct h pass = sendCommandS h (Acct pass)++rename :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse+rename h from to = do+  res <- sendCommand h (Rnfr from)+  case frStatus res of+    Continue -> sendCommandS h (Rnto to)+    _ -> return res++dele :: MIO.MonadIO m => Handle -> String -> m FTPResponse+dele h file = sendCommandS h (Dele file)++cwd :: MIO.MonadIO m => Handle -> String -> m FTPResponse+cwd h dir =+  sendCommandS h $+    if dir == ".."+      then Cdup+      else Cwd dir++size :: MIO.MonadIO m => Handle -> String -> m Int+size h file = do+  resp <- sendCommandS h (Size file)+  ensureCode resp 213+  return $ case frMessage resp of+    SingleLine message -> read . C.unpack $ message+    MultiLine _ -> 0++mkd :: MIO.MonadIO m => Handle -> String -> m String+mkd h dir = do+  resp <- sendCommandS h (Mkd dir)+  ensureCode resp 257+  parseResponse resp parse257++rmd :: MIO.MonadIO m => Handle -> String -> m FTPResponse+rmd h dir = sendCommandS h (Rmd dir)++pwd :: MIO.MonadIO m => Handle -> m String+pwd h = do+  resp <- sendCommandS h Pwd+  ensureCode resp 257+  parseResponse resp parse257++quit :: MIO.MonadIO m => Handle -> m FTPResponse+quit h = sendCommandS h Quit++mlst :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse+mlst h path = do+  resp <- sendCommandS h (Mlst path)+  case frMessage resp of+    SingleLine message -> return $ parseMlsxLine message+    MultiLine messages ->+      if length messages >= 2+        then return $ parseMlsxLine $ messages !! 1+        else MIO.liftIO $ Exception.throwIO $ BogusResponseFormatException resp++-- TLS commands++pbsz :: MIO.MonadIO m => Handle -> Int -> m FTPResponse+pbsz h = sendCommandS h . Pbsz++prot :: MIO.MonadIO m => Handle -> ProtType -> m FTPResponse+prot h = sendCommandS h . Prot++-- CCC and AUTH deliberately have no wrappers. CCC tells the server to drop to+-- cleartext, but there is no way to downgrade our side of a+-- crypton-connection, so the control connection would desynchronise and+-- 'security' could not be corrected to match. AUTH on its own tells the server+-- to expect a handshake that never happens; 'createTLSConnection' is the only+-- sequence that issues it correctly. Both remain reachable as 'FTPCommand'+-- constructors for anyone who needs to drive them by hand.++-- Data commands++{- | Rewrite line endings for a TYPE A transfer. RFC 959 specifies NVT-ASCII,+whose terminator is CRLF. Input already using CRLF is left alone rather than+having its CR doubled, and input with no final terminator does not gain one.+-}+toNetworkAscii :: ByteString -> ByteString+toNetworkAscii =+  let+    dropTrailingCR piece =+      if not (C.null piece) && C.last piece == '\r'+        then C.init piece+        else piece+  in+    C.intercalate (C.pack "\r\n") . fmap dropTrailingCR . C.split '\n'++sendType :: MIO.MonadIO m => RTypeCode -> ByteString -> Handle -> m ()+sendType TA dat h = MIO.liftIO . send h $ toNetworkAscii dat+sendType TI dat h = MIO.liftIO $ send h dat++withDataCommandSecurity ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  PortActivity ->+  RTypeCode ->+  FTPCommand ->+  (Handle -> m a) ->+  m a+withDataCommandSecurity h =+  case security h of+    Clear -> withDataCommand h+    TLS _ -> withTLSDataCommand h++nlst :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString+nlst h args = withDataCommandSecurity h Passive TA (Nlst args) getAllLineResp++retr :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m ByteString+retr h path = withDataCommandSecurity h Passive TI (Retr path) recvAll++list :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString+list h args = withDataCommandSecurity h Passive TA (List args) getAllLineResp++stor ::+  (MIO.MonadIO m, MonadMask m) =>+  Handle ->+  String ->+  B.ByteString ->+  RTypeCode ->+  m ()+stor h loc dat rtype =+  withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat++data MlsxResponse = MlsxResponse+  { mrFilename :: String+  , mrFacts :: Map String String+  }+  deriving (Show)++splitApart :: Char -> ByteString -> (ByteString, ByteString)+splitApart on s =+  let+    (x0, x1) = C.break (== on) s+  in+    (x0, C.drop 1 x1)++parseMlsxLine :: ByteString -> MlsxResponse+parseMlsxLine line =+  let+    (factLine, filename) = splitApart ' ' line+    bFacts = splitApart '=' <$> C.split ';' factLine+    facts =+      Map.fromList $+        filter (not . null . fst) $+          Monad.join (***) C.unpack <$> bFacts+  in+    MlsxResponse (C.unpack filename) facts++getMlsxResponse :: (MIO.MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse]+getMlsxResponse h =+  let+    collect :: MIO.MonadIO n => [MlsxResponse] -> n [MlsxResponse]+    collect ret = do+      mLine <- MIO.liftIO $ getLineRespMaybe h+      case mLine of+        Nothing -> return ret+        Just line ->+          collect $+            if C.null line+              then ret+              else parseMlsxLine line : ret+  in+    collect []++mlsd :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] mlsd h path = withDataCommandSecurity h Passive TA (Mlsd path) getMlsxResponse
test/test.hs view
@@ -1,74 +1,294 @@+module Main (main) where++import Control.Concurrent.MVar+import qualified Control.Exception as Exception import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C-import Test.Hspec+import qualified Network.Connection as Connection import Network.FTP.Client hiding (Success) import qualified Network.FTP.Client as F-import Control.Monad.IO.Class-import Control.Concurrent.MVar+import qualified System.Directory as Directory+import qualified System.IO as SIO+import System.IO.Error (eofErrorType, fullErrorType, isFullError, mkIOError)+import Test.Hspec  data TestHandleMVars = TestHandleMVars-    { thmSend :: MVar [ByteString]-    , thmSendLine :: MVar [ByteString]-    , thmRecv :: MVar [Int]-    }+  { thmSend :: MVar [ByteString]+  , _thmSendLine :: MVar [ByteString]+  , _thmRecv :: MVar [Int]+  }  data TestHandle = TestHandle TestHandleMVars Handle -testHandle-    :: [ByteString]-    -> [ByteString]-    -> Security-    -> IO TestHandle+testHandle ::+  [ByteString] ->+  [ByteString] ->+  Security ->+  IO TestHandle testHandle recvResps recvLineResps sec = do-    sendMVar <- newMVar []-    sendLineMVar <- newMVar []-    recvMVar <- newMVar []-    recvCount <- newMVar 0-    recvLineCount <- newMVar 0-    let testHandleMVars = TestHandleMVars-            sendMVar sendLineMVar recvMVar-        handle = Handle-            { send = \s ->-                modifyMVar_ sendMVar-                    (\ss -> return $ ss <> [s])-            , sendLine = \s ->-                modifyMVar_ sendLineMVar-                    (\ss -> return $ ss <> [s])-            , recv = \i -> do-                modifyMVar_ recvMVar-                    (\is -> return $ is <> [i])-                (recvResps !!) <$> modifyMVar recvCount-                    (\i -> return (i + 1, i))-            , recvLine =-                (recvLineResps !!) <$> modifyMVar recvLineCount-                    (\i -> return (i + 1, i))-            , security = sec-            }-    return $ TestHandle testHandleMVars handle+  sendMVar <- newMVar []+  sendLineMVar <- newMVar []+  recvMVar <- newMVar []+  recvCount <- newMVar 0+  recvLineCount <- newMVar 0+  let+    testHandleMVars =+      TestHandleMVars+        sendMVar+        sendLineMVar+        recvMVar+    handle =+      Handle+        { send = \s ->+            modifyMVar_+              sendMVar+              (\ss -> return $ ss <> [s])+        , sendLine = \s ->+            modifyMVar_+              sendLineMVar+              (\ss -> return $ ss <> [s])+        , recv = \i -> do+            modifyMVar_+              recvMVar+              (\is -> return $ is <> [i])+            nextScripted "recv" recvResps recvCount+        , recvLine = nextScripted "recvLine" recvLineResps recvLineCount+        , security = sec+        }+  return $ TestHandle testHandleMVars handle +{- | Hand back the next scripted response, or signal end of input the way a+real handle does once the peer has hung up.+-}+nextScripted :: String -> [ByteString] -> MVar Int -> IO ByteString+nextScripted what scripted countMVar = do+  i <- modifyMVar countMVar (\count -> return (count + 1, count))+  case drop i scripted of+    (x : _) -> return x+    [] -> ioError $ mkIOError eofErrorType what Nothing Nothing++{- | A handle whose reads fail the way a reset connection does, rather than the+way end of input does. The two must not be conflated: end of input is a+complete transfer, a reset is a truncated one.+-}+failingHandle :: Security -> IO Handle+failingHandle sec = do+  (TestHandle _ h) <- testHandle [] [] sec+  return+    h+      { recv = \_ -> ioError brokenConnection+      , recvLine = ioError brokenConnection+      }++brokenConnection :: IOError+brokenConnection = mkIOError fullErrorType "connection reset" Nothing Nothing+ main :: IO () main = hspec $ do-    describe "Network.FTP.Client.sendCommand" $ do-        it "sends USER for User" $ do-            let expected = FTPResponse-                    F.Success 200-                    (SingleLine $ C.pack "Ok")-            (TestHandle mvars h) <- testHandle [] [C.pack "200 Ok"] Clear-            sendCommand h (User "megan") `shouldReturn` expected-            takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"]-        it "sends USER for User and receives a multiline response" $ do-            let expected = FTPResponse-                    F.Success 200-                    (MultiLine [C.pack "line1", C.pack "line2", C.pack "200 line3"])-            (TestHandle mvars h) <- testHandle []-                [ C.pack "200-line1\r\n"-                , C.pack "line2\r\n"-                , C.pack "200 line3\r\n"-                ] Clear-            sendCommand h (User "megan") `shouldReturn` expected-            takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"]-    describe "Network.FTP.Client.recvAll" $-        it "doesn't hang on empty response" $ do-            let expected = C.pack ""-            (TestHandle mvars h) <- testHandle [C.pack ""] [] Clear-            recvAll h `shouldReturn` expected+  describe "Network.FTP.Client.sendCommand" $ do+    it "sends USER for User" $ do+      let+        expected =+          FTPResponse+            F.Success+            200+            (SingleLine $ C.pack "Ok")+      (TestHandle mvars h) <- testHandle [] [C.pack "200 Ok"] Clear+      sendCommand h (User "megan") `shouldReturn` expected+      takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"]+    it "sends USER for User and receives a multiline response" $ do+      let+        expected =+          FTPResponse+            F.Success+            200+            (MultiLine [C.pack "line1", C.pack "line2", C.pack "200 line3"])+      (TestHandle mvars h) <-+        testHandle+          []+          [ C.pack "200-line1\r\n"+          , C.pack "line2\r\n"+          , C.pack "200 line3\r\n"+          ]+          Clear+      sendCommand h (User "megan") `shouldReturn` expected+      takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"]+  describe "Network.FTP.Client.getResponse" $ do+    it "rejects an empty response line" $ do+      (TestHandle _ h) <- testHandle [] [C.pack ""] Clear+      getResponse h `shouldThrow` isBadProtocolResponse+    it "rejects a response line with a non numeric code" $ do+      (TestHandle _ h) <- testHandle [] [C.pack "abc def"] Clear+      getResponse h `shouldThrow` isBadProtocolResponse+    it "rejects a response line with a truncated code" $ do+      (TestHandle _ h) <- testHandle [] [C.pack "20 Ok"] Clear+      getResponse h `shouldThrow` isBadProtocolResponse+    it "accepts a bare code with no message" $ do+      let+        expected =+          FTPResponse+            F.Success+            200+            (SingleLine $ C.pack "")+      (TestHandle _ h) <- testHandle [] [C.pack "200"] Clear+      getResponse h `shouldReturn` expected+    it "keeps a blank line inside a multiline response" $ do+      -- RFC 959 lets the intermediate lines carry arbitrary text, so a+      -- blank line is reply content and must not end the response. Ending+      -- early would leave the real terminator unread and every later+      -- command would pick up the wrong reply.+      let+        expected =+          FTPResponse+            F.Success+            220+            ( MultiLine+                [ C.pack "First Line"+                , C.pack ""+                , C.pack "220 Third Line"+                ]+            )+      (TestHandle _ h) <-+        testHandle+          []+          [ C.pack "220-First Line\r\n"+          , C.pack "\r\n"+          , C.pack "220 Third Line\r\n"+          ]+          Clear+      getResponse h `shouldReturn` expected+    it "keeps reading continuation lines that repeat the code" $ do+      let+        expected =+          FTPResponse+            F.Success+            220+            ( MultiLine+                [ C.pack "First Line"+                , C.pack "220-Second Line"+                , C.pack "220 Third Line"+                ]+            )+      (TestHandle _ h) <-+        testHandle+          []+          [ C.pack "220-First Line\r\n"+          , C.pack "220-Second Line\r\n"+          , C.pack "220 Third Line\r\n"+          ]+          Clear+      getResponse h `shouldReturn` expected+    it "ends a multiline response on a bare code" $ do+      let+        expected =+          FTPResponse+            F.Success+            220+            (MultiLine [C.pack "First Line", C.pack "220"])+      (TestHandle _ h) <-+        testHandle+          []+          [ C.pack "220-First Line\r\n"+          , C.pack "220\r\n"+          ]+          Clear+      getResponse h `shouldReturn` expected+    it "does not end a multiline response on a different code" $ do+      let+        expected =+          FTPResponse+            F.Success+            220+            ( MultiLine+                [ C.pack "First Line"+                , C.pack "331 Not the terminator"+                , C.pack "220 Done"+                ]+            )+      (TestHandle _ h) <-+        testHandle+          []+          [ C.pack "220-First Line\r\n"+          , C.pack "331 Not the terminator\r\n"+          , C.pack "220 Done\r\n"+          ]+          Clear+      getResponse h `shouldReturn` expected+    it "rejects a multiline response the server never finished" $ do+      -- Terminating rather than hanging is only half of it. Handing back+      -- the fragment would report a successful 220 for a greeting that+      -- was cut off, against a control connection that is already gone.+      (TestHandle _ h) <-+        testHandle+          []+          [ C.pack "220-First Line\r\n"+          ]+          Clear+      getResponse h `shouldThrow` isBadProtocolResponse+  describe "Network.FTP.Client.recvAll" $ do+    it "doesn't hang on empty response" $ do+      let+        expected = C.pack ""+      (TestHandle _ h) <- testHandle [C.pack ""] [] Clear+      recvAll h `shouldReturn` expected+    it "reports a broken connection instead of a short read" $ do+      -- A failure part way through a transfer used to be turned into a clean+      -- end of data, so a truncated download could not be told apart from a+      -- complete one.+      h <- failingHandle Clear+      recvAll h `shouldThrow` isFullError+  describe "Network.FTP.Client.getAllLineResp" $+    it "reports a broken connection instead of a truncated listing" $ do+      h <- failingHandle Clear+      getAllLineResp h `shouldThrow` isFullError+  describe "Network.FTP.Client.sIOHandleImpl" $ do+    it "reads reply lines from a clear handle" $+      withBytesHandle (C.pack "220 Welcome\r\n331 Password\r\n") $ \h -> do+        getLineResp h `shouldReturn` C.pack "220 Welcome"+        getLineResp h `shouldReturn` C.pack "331 Password"+    it "hands back a final line the server never terminated" $+      -- Matches hGetLine, which this replaces: bytes did arrive, so they are+      -- the line, and end of input is reported on the read after it.+      withBytesHandle (C.pack "220 Welcome") $ \h -> do+        getLineResp h `shouldReturn` C.pack "220 Welcome"+        getLineRespMaybe h `shouldReturn` Nothing+    it "signals end of input once the stream is exhausted" $+      withBytesHandle (C.pack "220 Welcome\r\n") $ \h -> do+        _ <- getLineResp h+        getLineRespMaybe h `shouldReturn` Nothing+    it "refuses a reply line that exceeds the cap" $+      -- A server that never sends a newline must not be able to make us+      -- buffer without limit before we have even authenticated.+      withBytesHandle (C.replicate (maxReplyLineLength + 1) 'x') $ \h ->+        getLineResp h `shouldThrow` isLineTooLong+  describe "Network.FTP.Client.toNetworkAscii" $ do+    it "terminates LF input with CRLF" $+      toNetworkAscii (C.pack "a\nb\n") `shouldBe` C.pack "a\r\nb\r\n"+    it "leaves CRLF input unchanged rather than doubling the CR" $+      toNetworkAscii (C.pack "a\r\nb\r\n") `shouldBe` C.pack "a\r\nb\r\n"+    it "does not append a terminator the input did not have" $+      toNetworkAscii (C.pack "a\nb") `shouldBe` C.pack "a\r\nb"++{- | A real 'SIO.Handle' over fixed bytes. 'sIOHandleImpl' needs one, so the+scripted 'Handle' above cannot reach it.+-}+withBytesHandle :: ByteString -> (Handle -> IO a) -> IO a+withBytesHandle bytes use = do+  tmp <- Directory.getTemporaryDirectory+  Exception.bracket+    (SIO.openBinaryTempFile tmp "ftp-client-test")+    (\(path, h) -> SIO.hClose h >> Directory.removeFile path)+    ( \(path, h) -> do+        C.hPut h bytes+        SIO.hClose h+        SIO.withBinaryFile path SIO.ReadMode (use . sIOHandleImpl)+    )++isLineTooLong :: Connection.LineTooLong -> Bool+isLineTooLong _ = True++isBadProtocolResponse :: FTPException -> Bool+isBadProtocolResponse e =+  case e of+    BadProtocolResponseException _ -> True+    _ -> False