diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..336d822 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Haskell mass formatted with fourmolu +dbe4bbfa76ffcb567ff58a77a639814d567cdc5f diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5806b33..f84b168 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -64,6 +64,30 @@ jobs: - name: Build and test run: ./scripts/test --stack-yaml ${{ matrix.stack-yaml }} + formatting-check: + name: Formatting Check + if: >- + github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + # Nothing is compiled here, so there is no stack-root worth caching. The + # setup is still needed to establish the docker environment the scripts + # run in. + - name: Setup Stack (for docker env) + uses: flipstone/github-actions/setup-dockerized-stack@2c1f2781f8a1b92a0ad82223adf77bbf24b262c2 + with: + stack-root: ./stack-root + + - name: Format and Check for Diff + run: ./scripts/format-repo + cabal-check: name: Cabal Check if: >- diff --git a/example/Setup.hs b/example/Setup.hs index 9a994af..e8ef27d 100644 --- a/example/Setup.hs +++ b/example/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/example/app/Main.hs b/example/app/Main.hs index ae962e0..24d5570 100644 --- a/example/app/Main.hs +++ b/example/app/Main.hs @@ -1,14 +1,14 @@ module Main (main) where -import Network.FTP.Client -import qualified Network.FTP.Client.Conduit as FC import Conduit import qualified Data.ByteString.Char8 as C +import Network.FTP.Client +import qualified Network.FTP.Client.Conduit as FC main :: IO () main = withFTPS "hostname.com" 21 $ \h _welcome -> do - _ <- login h "username" "password" - runConduitRes - $ FC.mlsd h "." - .| mapC (C.pack . (<> "\n") . show) - .| stdoutC + _ <- login h "username" "password" + runConduitRes $ + FC.mlsd h "." + .| mapC (C.pack . (<> "\n") . show) + .| stdoutC diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..e31b827 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,14 @@ +indentation: 2 +function-arrows: trailing +comma-style: leading # default +import-export-style: leading +indent-wheres: false # default +record-brace-space: true +newlines-between-decls: 1 # default +haddock-style: multi-line # default +haddock-style-module: # default +let-style: newline +in-style: left-align +unicode: never # default +respectful: true # default +single-constraint-parens: never diff --git a/ftp-client-conduit/CHANGELOG.md b/ftp-client-conduit/CHANGELOG.md index 073de77..2ee7b8e 100644 --- a/ftp-client-conduit/CHANGELOG.md +++ b/ftp-client-conduit/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog for ftp-client-conduit +## 0.5.0.8 + +* 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. + +* Correct the Haddock module header, which named `Network.FTP.Client` rather + than `Network.FTP.Client.Conduit`. + ## 0.5.0.7 * Add the missing upper bounds on `bytestring`, `conduit` and `exceptions`. diff --git a/ftp-client-conduit/Setup.hs b/ftp-client-conduit/Setup.hs index 9a994af..e8ef27d 100644 --- a/ftp-client-conduit/Setup.hs +++ b/ftp-client-conduit/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/ftp-client-conduit/ftp-client-conduit.cabal b/ftp-client-conduit/ftp-client-conduit.cabal index 32e6ba8..cd15f83 100644 --- a/ftp-client-conduit/ftp-client-conduit.cabal +++ b/ftp-client-conduit/ftp-client-conduit.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: ftp-client-conduit -version: 0.5.0.7 +version: 0.5.0.8 synopsis: Transfer file with FTP and FTPS with Conduit description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. ftp-client-conduit uses conduit to stream files and data in constant space. category: Web @@ -63,6 +63,10 @@ library 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-conduit-test type: exitcode-stdio-1.0 diff --git a/ftp-client-conduit/henforcer.toml b/ftp-client-conduit/henforcer.toml new file mode 100644 index 0000000..f508849 --- /dev/null +++ b/ftp-client-conduit/henforcer.toml @@ -0,0 +1,10 @@ +[forAnyModule] +allowedOpenUnaliasedImports = 1 +moduleHeaderCopyrightMustExistNonEmpty = true +moduleHeaderDescriptionMustExistNonEmpty = false +moduleHeaderLicenseMustExistNonEmpty = true +moduleHeaderMaintainerMustExistNonEmpty = false +[[forPatternModules]] +pattern = "Paths_*" +[forPatternModules.rulesToIgnore] +all = true diff --git a/ftp-client-conduit/package.yaml b/ftp-client-conduit/package.yaml index 0e2ca2a..20ecde2 100644 --- a/ftp-client-conduit/package.yaml +++ b/ftp-client-conduit/package.yaml @@ -1,5 +1,5 @@ name: ftp-client-conduit -version: 0.5.0.7 +version: 0.5.0.8 synopsis: Transfer file with FTP and FTPS with Conduit description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. ftp-client-conduit uses conduit to stream files and data in constant space. homepage: https://github.com/flipstone/ftp-client @@ -82,6 +82,11 @@ library: - condition: impl (ghc >= 9.10) ghc-options: - -Wdefaulted-exception-context + - condition: impl (ghc >= 9.6) + dependencies: + - henforcer + ghc-options: + - -fplugin Henforcer tests: ftp-conduit-test: diff --git a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs index 8b9dc17..0365522 100644 --- a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs +++ b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs @@ -1,209 +1,226 @@ {-# LANGUAGE ScopedTypeVariables #-} -{-| -Module : Network.FTP.Client +{- | +Module : Network.FTP.Client.Conduit Description : Transfer files over FTP and FTPS with Conduit +Copyright : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026 License : Public Domain Stability : experimental Portability : POSIX -} -module Network.FTP.Client.Conduit ( - -- * Data commands - nlst, - retr, - list, - stor, - mlsd -) where - --- MonadResource appears in this module's exported signatures, so it is taken --- from resourcet directly rather than through Conduit's re-export. That keeps --- the resourcet dependency honest instead of implicit. -import Conduit hiding (MonadResource) +module Network.FTP.Client.Conduit + ( -- * Data commands + nlst + , retr + , list + , stor + , mlsd + ) where + +-- MonadResource appears in this module's exported signatures. Conduit +-- re-exports it, so importing it from resourcet by name is what keeps the +-- resourcet dependency genuinely used rather than implicit. + +import Conduit ((.|)) +import qualified Conduit +import qualified Control.Monad.IO.Class as MIO import Control.Monad.Trans.Resource (MonadResource) import Data.ByteString.Lazy.Internal (defaultChunkSize) -import System.IO import Network.FTP.Client - ( sendCommandS - , FTPCommand(..) - , RTypeCode(..) - , createSendDataCommand - , createTLSSendDataCommand - , PortActivity(..) - , getResponse - , sIOHandleImpl - , tlsHandleImpl - , Security(..) - , parseMlsxLine - ) + ( FTPCommand (..) + , PortActivity (..) + , RTypeCode (..) + , Security (..) + , createSendDataCommand + , createTLSSendDataCommand + , getResponse + , parseMlsxLine + , sIOHandleImpl + , sendCommandS + , tlsHandleImpl + ) +import qualified System.IO as SIO -import qualified Network.FTP.Client as FTP -import qualified Data.ByteString as B -import Data.ByteString (ByteString) -import Network.Connection import qualified Control.Monad.Catch as M +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import qualified Network.Connection as Connection +import qualified Network.FTP.Client as FTP debugging :: Bool debugging = False -debugPrint :: (Show a, MonadIO m) => a -> m () +debugPrint :: (Show a, MIO.MonadIO m) => a -> m () debugPrint s = - if debugging - then liftIO $ print s - else return () + if debugging + then MIO.liftIO $ print s + else return () -debugResponse :: (Show a, MonadIO m) => a -> m () +debugResponse :: (Show a, MIO.MonadIO m) => a -> m () debugResponse s = debugPrint $ "Recieved: " <> (show s) -getAllLineRespC - :: forall i m - . MonadIO m - => FTP.Handle - -> ConduitT i ByteString m () +getAllLineRespC :: + forall i m. + MIO.MonadIO m => + FTP.Handle -> + Conduit.ConduitT i ByteString m () getAllLineRespC h = - let loop :: ConduitT i ByteString m () - loop = do - line <- liftIO - $ FTP.getLineResp h `M.catchIOError` const (return "") - if B.null line - then return () - else do - yield line - loop - in loop - -sendAllLineC - :: forall o m - . MonadIO m - => FTP.Handle - -> ConduitT ByteString o m () + let + loop :: Conduit.ConduitT i ByteString m () + loop = do + line <- + MIO.liftIO $ + FTP.getLineResp h `M.catchIOError` const (return "") + if B.null line + then return () + else do + Conduit.yield line + loop + in + loop + +sendAllLineC :: + forall o m. + MIO.MonadIO m => + FTP.Handle -> + Conduit.ConduitT ByteString o m () sendAllLineC h = - let loop :: ConduitT ByteString o m () - loop = do - mx <- await - case mx of - Nothing -> return () - Just x -> do - liftIO $ FTP.sendLine h x - loop - in loop - -sourceDataCommandSecurity - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + let + loop :: Conduit.ConduitT ByteString o m () + loop = do + mx <- Conduit.await + case mx of + Nothing -> return () + Just x -> do + MIO.liftIO $ FTP.sendLine h x + loop + in + loop + +sourceDataCommandSecurity :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceDataCommandSecurity h = - case FTP.security h of - Clear -> sourceDataCommand h - TLS -> sourceTLSDataCommand h - -sourceDataCommand - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + case FTP.security h of + Clear -> sourceDataCommand h + TLS -> sourceTLSDataCommand h + +sourceDataCommand :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- bracketP - (createSendDataCommand ch pa cmd) - (liftIO . hClose) - (f . sIOHandleImpl) - resp <- getResponse ch - debugResponse resp - return x - -sourceTLSDataCommand - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + _ <- sendCommandS ch $ RType code + x <- + Conduit.bracketP + (createSendDataCommand ch pa cmd) + (MIO.liftIO . SIO.hClose) + (f . sIOHandleImpl) + resp <- getResponse ch + debugResponse resp + return x + +sourceTLSDataCommand :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceTLSDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- bracketP - (createTLSSendDataCommand ch pa cmd) - (liftIO . connectionClose) - (f . tlsHandleImpl) - resp <- getResponse ch - debugResponse resp - return x - -sourceFTPHandle - :: forall i m - . MonadIO m - => FTP.Handle - -> ConduitT i ByteString m () + _ <- sendCommandS ch $ RType code + x <- + Conduit.bracketP + (createTLSSendDataCommand ch pa cmd) + (MIO.liftIO . Connection.connectionClose) + (f . tlsHandleImpl) + resp <- getResponse ch + debugResponse resp + return x + +sourceFTPHandle :: + forall i m. + MIO.MonadIO m => + FTP.Handle -> + Conduit.ConduitT i ByteString m () sourceFTPHandle h = - let loop :: ConduitT i ByteString m () - loop = do - bs <- liftIO $ FTP.recv h defaultChunkSize - `M.catchIOError` const (return "") - if B.null bs - then return () - else do - yield bs - loop - in loop - -sinkFTPHandle - :: forall o m - . MonadIO m - => FTP.Handle - -> ConduitT ByteString o m () + let + loop :: Conduit.ConduitT i ByteString m () + loop = do + bs <- + MIO.liftIO $ + FTP.recv h defaultChunkSize + `M.catchIOError` const (return "") + if B.null bs + then return () + else do + Conduit.yield bs + loop + in + loop + +sinkFTPHandle :: + forall o m. + MIO.MonadIO m => + FTP.Handle -> + Conduit.ConduitT ByteString o m () sinkFTPHandle h = - let loop :: ConduitT ByteString o m () - loop = do - mbs <- await - case mbs of - Nothing -> return () - Just bs -> do - liftIO $ FTP.send h bs - loop - in loop - -sendType - :: MonadResource m - => RTypeCode - -> FTP.Handle - -> ConduitT ByteString o m () + let + loop :: Conduit.ConduitT ByteString o m () + loop = do + mbs <- Conduit.await + case mbs of + Nothing -> return () + Just bs -> do + MIO.liftIO $ FTP.send h bs + loop + in + loop + +sendType :: + MonadResource m => + RTypeCode -> + FTP.Handle -> + Conduit.ConduitT ByteString o m () sendType TA h = sendAllLineC h sendType TI h = sinkFTPHandle h -nlst :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () +nlst :: MonadResource m => FTP.Handle -> [String] -> Conduit.ConduitT i ByteString m () nlst ch args = - sourceDataCommandSecurity ch Passive TA (Nlst args) getAllLineRespC + sourceDataCommandSecurity ch Passive TA (Nlst args) getAllLineRespC -retr :: MonadResource m => FTP.Handle -> String -> ConduitT i ByteString m () +retr :: MonadResource m => FTP.Handle -> String -> Conduit.ConduitT i ByteString m () retr ch path = - sourceDataCommandSecurity ch Passive TI (Retr path) sourceFTPHandle + sourceDataCommandSecurity ch Passive TI (Retr path) sourceFTPHandle -list :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () +list :: MonadResource m => FTP.Handle -> [String] -> Conduit.ConduitT i ByteString m () list ch args = - sourceDataCommandSecurity ch Passive TA (List args) getAllLineRespC - -stor - :: MonadResource m - => FTP.Handle - -> String - -> RTypeCode - -> ConduitT ByteString o m () + sourceDataCommandSecurity ch Passive TA (List args) getAllLineRespC + +stor :: + MonadResource m => + FTP.Handle -> + String -> + RTypeCode -> + Conduit.ConduitT ByteString o m () stor ch loc rtype = - sourceDataCommandSecurity ch Passive rtype (Stor loc) $ sendType rtype + sourceDataCommandSecurity ch Passive rtype (Stor loc) $ sendType rtype -mlsd - :: MonadResource m - => FTP.Handle - -> String - -> ConduitT i FTP.MlsxResponse m () +mlsd :: + MonadResource m => + FTP.Handle -> + String -> + Conduit.ConduitT i FTP.MlsxResponse m () mlsd ch dir = - sourceDataCommandSecurity ch Passive TA (Mlsd dir) getAllLineRespC - .| mapC parseMlsxLine + sourceDataCommandSecurity ch Passive TA (Mlsd dir) getAllLineRespC + .| Conduit.mapC parseMlsxLine diff --git a/ftp-client/CHANGELOG.md b/ftp-client/CHANGELOG.md index 588269c..139d48f 100644 --- a/ftp-client/CHANGELOG.md +++ b/ftp-client/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for ftp-client +## 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 diff --git a/ftp-client/Setup.hs b/ftp-client/Setup.hs index 9a994af..e8ef27d 100644 --- a/ftp-client/Setup.hs +++ b/ftp-client/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/ftp-client/ftp-client.cabal b/ftp-client/ftp-client.cabal index c6993fc..065169b 100644 --- a/ftp-client/ftp-client.cabal +++ b/ftp-client/ftp-client.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: ftp-client -version: 0.5.3.0 +version: 0.5.3.1 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 @@ -64,6 +64,10 @@ library 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 diff --git a/ftp-client/henforcer.toml b/ftp-client/henforcer.toml new file mode 100644 index 0000000..f508849 --- /dev/null +++ b/ftp-client/henforcer.toml @@ -0,0 +1,10 @@ +[forAnyModule] +allowedOpenUnaliasedImports = 1 +moduleHeaderCopyrightMustExistNonEmpty = true +moduleHeaderDescriptionMustExistNonEmpty = false +moduleHeaderLicenseMustExistNonEmpty = true +moduleHeaderMaintainerMustExistNonEmpty = false +[[forPatternModules]] +pattern = "Paths_*" +[forPatternModules.rulesToIgnore] +all = true diff --git a/ftp-client/package.yaml b/ftp-client/package.yaml index 70400cc..cf18b69 100644 --- a/ftp-client/package.yaml +++ b/ftp-client/package.yaml @@ -1,5 +1,5 @@ name: ftp-client -version: 0.5.3.0 +version: 0.5.3.1 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 @@ -83,6 +83,11 @@ library: - condition: impl (ghc >= 9.10) ghc-options: - -Wdefaulted-exception-context + - condition: impl (ghc >= 9.6) + dependencies: + - henforcer + ghc-options: + - -fplugin Henforcer tests: ftp-client-test: diff --git a/ftp-client/src/Network/FTP/Client.hs b/ftp-client/src/Network/FTP/Client.hs index d114fdc..4c8a2f9 100644 --- a/ftp-client/src/Network/FTP/Client.hs +++ b/ftp-client/src/Network/FTP/Client.hs @@ -1,159 +1,178 @@ -{-| +{- | 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, +module Network.FTP.Client + ( -- * Main Entrypoints + withFTP + , withFTPS + -- * Control Commands - login, - pasv, - rename, - dele, - cwd, - size, - acct, - mkd, - rmd, - pwd, - quit, + , login + , pasv + , rename + , dele + , cwd + , size + , acct + , mkd + , rmd + , pwd + , quit + -- * Data Commands - nlst, - retr, - list, - stor, - mlsd, - mlst, + , nlst + , retr + , list + , stor + , mlsd + , mlst + -- * Types - FTPCommand(..), - FTPResponse(..), - FTPMessage(..), - ResponseStatus(..), - MlsxResponse(..), - RTypeCode(..), - PortActivity(..), - ProtType(..), - Security(..), - Handle(..), + , FTPCommand (..) + , FTPResponse (..) + , FTPMessage (..) + , ResponseStatus (..) + , MlsxResponse (..) + , RTypeCode (..) + , PortActivity (..) + , ProtType (..) + , Security (..) + , Handle (..) + -- * TLS Commands - pbsz, - prot, - ccc, - auth, + , pbsz + , prot + , ccc + , auth + -- * Exceptions - FTPException(..), + , FTPException (..) + -- * System Handle Creation - createSIOHandle, - createTLSConnection, - connectTLS, + , createSIOHandle + , createTLSConnection + , connectTLS + -- * Handle Implementations - sIOHandleImpl, - tlsHandleImpl, - -- * Lower Level Functions - sendCommand, - sendCommandS, - recvAll, - sendAll, - sendAllS, - getLineResp, - getResponse, - getResponseS, - sendCommandLine, - createSendDataCommand, - createTLSSendDataCommand, - parseMlsxLine -) where + , sIOHandleImpl + , tlsHandleImpl -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 (intercalate) -import Data.Attoparsec.ByteString.Char8 -import qualified Network.Socket as S -import qualified System.IO as SIO -import Control.Exception + -- * Lower Level Functions + , sendCommand + , sendCommandS + , recvAll + , sendAll + , sendAllS + , getLineResp + , getResponse + , getResponseS + , sendCommandLine + , createSendDataCommand + , createTLSSendDataCommand + , 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 Control.Monad -import Control.Monad.IO.Class -import Data.Bits -import Network.Connection +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.List (intercalate) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map -import Control.Arrow +import qualified Network.Connection as Connection +import qualified Network.Socket as S +import qualified System.IO as SIO import System.IO.Error (isEOFError) debugging :: Bool debugging = False -debugPrint :: (Show a, MonadIO m) => a -> m () -debugPrint s = when debugging (liftIO $ print s) +debugPrint :: (Show a, MIO.MonadIO m) => a -> m () +debugPrint s = Monad.when debugging (MIO.liftIO $ print s) -debugResponse :: (Show a, MonadIO m) => a -> m () +debugResponse :: (Show a, MIO.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 - } + { send :: ByteString -> IO () + , sendLine :: ByteString -> IO () + , recv :: Int -> IO ByteString + , recvLine :: IO ByteString + , security :: Security + } data FTPMessage = SingleLine ByteString | MultiLine [ByteString] - deriving Eq + deriving (Eq) instance Show FTPMessage where - show (SingleLine message) = C.unpack message - show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages + 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 +data FTPResponse = FTPResponse + { frStatus :: ResponseStatus + -- ^ Interpretation of the first AC.digit of an FTP response code + , frCode :: Int + -- ^ The three AC.digit response code + , frMessage :: FTPMessage + -- ^ Text of the response + } + deriving (Eq) instance Show FTPResponse where - show fr = show (frCode fr) <> " " <> show (frMessage fr) + show fr = show (frCode fr) <> " " <> show (frMessage fr) --- | First digit of an FTP response +-- | First AC.digit of an FTP response data ResponseStatus - = Wait -- ^ 1 - | Success -- ^ 2 - | Continue -- ^ 3 - | FailureRetry -- ^ 4 - | Failure -- ^ 5 - deriving (Show, Eq) + = -- | 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) + = FailureRetryException FTPResponse + | FailureException FTPResponse + | UnsuccessfulException FTPResponse + | BogusResponseFormatException FTPResponse + | BadProtocolResponseException ByteString + deriving (Show) -instance Exception FTPException +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 - _ -> throw $ BadProtocolResponseException 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 @@ -167,75 +186,77 @@ 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 + = 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 + 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) + 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 (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 (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" +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') @@ -244,172 +265,193 @@ stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n') 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. +{- | 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 :: MonadIO m => Handle -> m FTPResponse + (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 <- 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. - when (C.length code < 3 || not (C.all isDigit code)) - $ liftIO $ 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 -> liftIO $ throwIO $ FailureRetryException response - Failure -> liftIO $ throwIO $ FailureException response - _ -> return response - -loopMultiLine - :: MonadIO m - => Handle - -> ByteString - -> [ByteString] - -> m [ByteString] + 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 <- liftIO $ getLineRespMaybe h - case mNextLine of - -- The server hung up before sending the terminating line. Return what - -- was collected rather than looping forever. Note 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 -> return 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 :: MonadIO m => FTPResponse -> m FTPResponse + mNextLine <- MIO.liftIO $ getLineRespMaybe h + case mNextLine of + -- The server hung up before sending the terminating line. Return what + -- was collected rather than looping forever. Note 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 -> return 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 - _ -> liftIO $ throwIO $ UnsuccessfulException resp + case frStatus resp of + Success -> return resp + _ -> MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp -getResponseS :: MonadIO m => Handle -> m FTPResponse +getResponseS :: MIO.MonadIO m => Handle -> m FTPResponse getResponseS = ensureSuccess <=< getResponse -sendCommandLine :: MonadIO m => Handle -> ByteString -> m () -sendCommandLine h = liftIO . send h . (<> "\r\n") +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 :: MonadIO m => Handle -> FTPCommand -> m FTPResponse +{- | 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 :: MonadIO m => Handle -> FTPCommand -> m FTPResponse + 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 :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] +{- | Equvalent to + +> mapM . sendCommand +-} +sendAll :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] sendAll = mapM . sendCommand --- | Equvalent to --- --- > mapM . sendCommandS -sendAllS :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] +{- | Equvalent to + +> mapM . sendCommandS +-} +sendAllS :: MIO.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 :: + MIO.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 + 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 - :: (MonadIO m, MonadMask m) - => String - -> Int - -> (S.Socket -> m a) - -> m a + 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 - } - M.bracketOnError - (createSocket (Just host) portNum hints) - (liftIO . S.close . fst) - (\(sock, addr) -> do - debugPrint ("Connecting" :: String) - liftIO $ S.connect sock (S.addrAddress addr) - debugPrint ("Connected" :: String) - f sock - ) - -withSocketActive :: (MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a + let + hints = + S.defaultHints + { S.addrSocketType = S.Stream + } + 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] - } - M.bracketOnError - (createSocket Nothing 0 hints) - (liftIO . S.close . fst) - (\(sock, addr) -> do - debugPrint ("Binding" :: String) - liftIO $ S.bind sock (S.addrAddress addr) - liftIO $ S.listen sock 1 - debugPrint ("Listening" :: String) - f sock - ) - -createSIOHandle :: (MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle -createSIOHandle host portNum = withSocketPassive host portNum - $ liftIO . flip S.socketToHandle SIO.ReadWriteMode + let + hints = + S.defaultHints + { S.addrSocketType = S.Stream + , S.addrFlags = [S.AI_PASSIVE] + } + M.bracketOnError + (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 +sIOHandleImpl h = + Handle { send = C.hPut h , sendLine = C.hPutStrLn h , recv = C.hGetSome h @@ -417,435 +459,477 @@ sIOHandleImpl h = Handle , 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) +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 - :: (MonadIO m, MonadMask m) - => String - -> Int - -> (Handle -> FTPResponse -> m a) - -> m a +{- | 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 + resp <- getResponse h + f h resp -- Data connection -withDataSocketPasv - :: (MonadIO m, MonadMask m) - => Handle - -> (S.Socket -> m a) - -> m a +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 - :: (MonadIO m, MonadMask m) - => Handle - -> (S.Socket -> m a) - -> m a + (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) <- liftIO $ do - (S.SockAddrInet p hostAddr) <- S.getSocketName socket - return (p,hostAddr) - _ <- port h sHost sPort - f socket + (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 - :: (MonadIO m, MonadMask m) - => PortActivity - -> Handle - -> (S.Socket -> m a) - -> m a -withDataSocket Active = withDataSocketActive +withDataSocket :: + (MIO.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 :: MIO.MonadIO m => PortActivity -> S.Socket -> m S.Socket acceptData Passive = return -acceptData Active = return . fst <=< liftIO . S.accept +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 :: MonadIO m => Handle -> FTPResponse -> m () +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 - 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 + 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 -> + 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 + resp <- sendCommand h cmd + ensureSucessfulData h resp + acceptedSock <- acceptData pa socket + MIO.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 :: + (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 - x <- M.bracket - (createSendDataCommand ch pa cmd) - (liftIO . SIO.hClose) - (f . sIOHandleImpl) - resp <- getResponse ch - debugResponse resp - return x + _ <- sendCommandS ch $ RType code + x <- + M.bracket + (createSendDataCommand ch pa cmd) + (MIO.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 :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString getAllLineResp h = - let collect :: (MonadIO n, MonadCatch n) => [ByteString] -> n ByteString - collect ret = ( do - line <- liftIO $ getLineResp h - collect (ret <> [line])) - `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret) - in collect [] + let + collect :: (MIO.MonadIO n, MonadCatch n) => [ByteString] -> n ByteString + collect ret = + ( do + line <- MIO.liftIO $ getLineResp h + collect (ret <> [line]) + ) + `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret) + in + collect [] -- | Recieve all data and return it as a 'Data.ByteString.ByteString' -recvAll :: (MonadIO m, MonadCatch m) => Handle -> m ByteString +recvAll :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString recvAll h = - let collect :: (MonadIO n, MonadCatch n) => ByteString -> n ByteString - collect bs = ( do - chunk <- liftIO $ recv h defaultChunkSize - if C.null chunk - then return bs - else collect $ bs <> chunk - ) `M.catchIOError` (\_ -> return bs) - in collect "" + let + collect :: (MIO.MonadIO n, MonadCatch n) => ByteString -> n ByteString + collect bs = + ( do + chunk <- MIO.liftIO $ recv h defaultChunkSize + if C.null chunk + then return bs + else collect $ bs <> chunk + ) + `M.catchIOError` (\_ -> return bs) + in + collect "" -- TLS connection -connectTLS :: MonadIO m => SIO.Handle -> String -> Int -> m Connection +connectTLS :: MIO.MonadIO m => SIO.Handle -> String -> Int -> m Connection.Connection connectTLS h host portNum = do - context <- liftIO initConnectionContext - let tlsSettings = case def of - simpleSettings@TLSSettingsSimple{} -> - simpleSettings { settingDisableCertificateValidation = True } - otherSettings -> otherSettings - 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) + context <- MIO.liftIO Connection.initConnectionContext + let + tlsSettings = case def of + simpleSettings@Connection.TLSSettingsSimple {} -> + simpleSettings {Connection.settingDisableCertificateValidation = True} + otherSettings -> otherSettings + connectionParams = + Connection.ConnectionParams + { Connection.connectionHostname = host + , Connection.connectionPort = toEnum . fromEnum $ portNum + , Connection.connectionUseSecure = Just tlsSettings + , Connection.connectionUseSocks = Nothing + } + MIO.liftIO $ Connection.connectFromHandle context h connectionParams + +createTLSConnection :: + (MIO.MonadIO m, MonadMask m) => + String -> + Int -> + m (FTPResponse, Connection.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 + h <- createSIOHandle host portNum + let + insecureH = sIOHandleImpl h + resp <- getResponse insecureH + _ <- sendCommand insecureH Auth + conn <- connectTLS h host portNum + return (resp, conn) + +tlsHandleImpl :: Connection.Connection -> Handle +tlsHandleImpl c = + Handle + { send = Connection.connectionPut c + , sendLine = Connection.connectionPut c . (<> "\n") + , recv = Connection.connectionGet c + , recvLine = Connection.connectionGetLine maxBound c , security = TLS } -withTLSHandle - :: (MonadMask m, MonadIO m) - => String - -> Int - -> (Handle -> FTPResponse -> m a) - -> m a -withTLSHandle host portNum f = M.bracket +withTLSHandle :: + (MonadMask m, MIO.MonadIO m) => + String -> + Int -> + (Handle -> FTPResponse -> m a) -> + m a +withTLSHandle host portNum f = + M.bracket (createTLSConnection host portNum) - (liftIO . connectionClose . snd) + (MIO.liftIO . Connection.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 +{- | 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, MIO.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 +{- | Send setup commands to the server and +create a data TLS connection +-} +createTLSSendDataCommand :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + FTPCommand -> + m Connection.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 + _ <- sendAllS ch [Pbsz 0, Prot P] + withDataSocket pa ch $ \socket -> do + resp <- sendCommand ch cmd + ensureSucessfulData ch resp + acceptedSock <- acceptData pa socket + (sPort, sHost) <- MIO.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 <- MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode + MIO.liftIO $ connectTLS h hostName (fromEnum sPort) + +withTLSDataCommand :: + (MIO.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 + _ <- sendCommandS ch $ RType code + x <- + M.bracket + (createTLSSendDataCommand ch pa cmd) + (MIO.liftIO . Connection.connectionClose) + (f . tlsHandleImpl) + 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 parseOnly p parsableMessage of - Right x -> return x - Left _ -> liftIO $ throwIO - $ BadProtocolResponseException parsableMessage - -ensureCode :: MonadIO m => FTPResponse -> Int -> m () + 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 = - liftIO $ when (frCode resp /= code) - $ liftIO $ throwIO $ UnsuccessfulException resp + MIO.liftIO $ + Monad.when (frCode resp /= code) $ + MIO.liftIO $ + Exception.throwIO $ + UnsuccessfulException resp -parse227 :: Parser (String, Int) +parse227 :: AC.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 + _ <- 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 - _ <- char '"' - C.unpack <$> takeTill (== '"') + _ <- AC.char '"' + C.unpack <$> AC.takeTill (== '"') -- Control commands -login :: MonadIO m => Handle -> String -> String -> m FTPResponse +login :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse login h user pass = do - resp <- last <$> sendAll h [User user, Pass pass] - ensureSuccess resp + resp <- last <$> sendAll h [User user, Pass pass] + ensureSuccess resp -pasv :: MonadIO m => Handle -> m (String, Int) +pasv :: MIO.MonadIO m => Handle -> m (String, Int) pasv h = do - resp <- sendCommandS h Pasv - ensureCode resp 227 - parseResponse resp parse227 + resp <- sendCommandS h Pasv + ensureCode resp 227 + parseResponse resp parse227 -port :: MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse +port :: MIO.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 :: MIO.MonadIO m => Handle -> String -> m FTPResponse acct h pass = sendCommandS h (Acct pass) -rename :: MonadIO m => Handle -> String -> String -> m FTPResponse +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 + res <- sendCommand h (Rnfr from) + case frStatus res of + Continue -> sendCommandS h (Rnto to) + _ -> return res -dele :: MonadIO m => Handle -> String -> m FTPResponse +dele :: MIO.MonadIO m => Handle -> String -> m FTPResponse dele h file = sendCommandS h (Dele file) -cwd :: MonadIO m => Handle -> String -> m FTPResponse +cwd :: MIO.MonadIO m => Handle -> String -> m FTPResponse cwd h dir = - sendCommandS h $ if dir == ".." - then Cdup - else Cwd dir + sendCommandS h $ + if dir == ".." + then Cdup + else Cwd dir -size :: MonadIO m => Handle -> String -> m Int +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 + 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 :: MIO.MonadIO m => Handle -> String -> m String mkd h dir = do - resp <- sendCommandS h (Mkd dir) - ensureCode resp 257 - parseResponse resp parse257 + resp <- sendCommandS h (Mkd dir) + ensureCode resp 257 + parseResponse resp parse257 -rmd :: MonadIO m => Handle -> String -> m FTPResponse +rmd :: MIO.MonadIO m => Handle -> String -> m FTPResponse rmd h dir = sendCommandS h (Rmd dir) -pwd :: MonadIO m => Handle -> m String +pwd :: MIO.MonadIO m => Handle -> m String pwd h = do - resp <- sendCommandS h Pwd - ensureCode resp 257 - parseResponse resp parse257 + resp <- sendCommandS h Pwd + ensureCode resp 257 + parseResponse resp parse257 -quit :: MonadIO m => Handle -> m FTPResponse +quit :: MIO.MonadIO m => Handle -> m FTPResponse quit h = sendCommandS h Quit -mlst :: (MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse +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 liftIO $ throwIO $ BogusResponseFormatException resp + 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 :: MonadIO m => Handle -> Int -> m FTPResponse +pbsz :: MIO.MonadIO m => Handle -> Int -> m FTPResponse pbsz h = sendCommandS h . Pbsz -prot :: MonadIO m => Handle -> ProtType -> m FTPResponse +prot :: MIO.MonadIO m => Handle -> ProtType -> m FTPResponse prot h = sendCommandS h . Prot -ccc :: MonadIO m => Handle -> m FTPResponse +ccc :: MIO.MonadIO m => Handle -> m FTPResponse ccc h = sendCommandS h Ccc -auth :: MonadIO m => Handle -> m FTPResponse +auth :: MIO.MonadIO m => Handle -> m FTPResponse auth h = sendCommandS h Auth -- Data commands -sendType :: MonadIO m => RTypeCode -> ByteString -> Handle -> m () +sendType :: MIO.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 +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 + case security h of + Clear -> withDataCommand h + TLS -> withTLSDataCommand h -nlst :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString +nlst :: (MIO.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 :: (MIO.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 :: (MIO.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 :: + (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 + withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat -data MlsxResponse = MlsxResponse { - mrFilename :: String, - mrFacts :: Map String String -} deriving (Show) +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) + 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] + 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 :: (MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] - collect ret = ( do - line <- liftIO $ getLineResp h - collect $ - if C.null line - then ret - else parseMlsxLine line : ret - ) `M.catchIOError` (\_ -> return ret) - in collect [] - -mlsd :: (MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] + let + collect :: (MIO.MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] + collect ret = + ( do + line <- MIO.liftIO $ getLineResp h + collect $ + if C.null line + then ret + else parseMlsxLine line : ret + ) + `M.catchIOError` (\_ -> return ret) + in + collect [] + +mlsd :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] mlsd h path = withDataCommandSecurity h Passive TA (Mlsd path) getMlsxResponse diff --git a/ftp-client/test/test.hs b/ftp-client/test/test.hs index eeb72c6..417c8b3 100644 --- a/ftp-client/test/test.hs +++ b/ftp-client/test/test.hs @@ -1,167 +1,222 @@ module Main (main) where +import Control.Concurrent.MVar import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C -import Test.Hspec import Network.FTP.Client hiding (Success) import qualified Network.FTP.Client as F -import Control.Concurrent.MVar import System.IO.Error (eofErrorType, 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]) - nextScripted "recv" recvResps recvCount - , recvLine = nextScripted "recvLine" recvLineResps recvLineCount - , 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. +{- | 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 + i <- modifyMVar countMVar (\count -> return (count + 1, count)) + case drop i scripted of + (x : _) -> return x + [] -> ioError $ mkIOError eofErrorType what 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.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 "stops when the server hangs up during a multiline response" $ do - let expected = FTPResponse - F.Success 220 - (MultiLine [C.pack "First Line"]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - ] Clear - getResponse h `shouldReturn` expected - describe "Network.FTP.Client.recvAll" $ - it "doesn't hang on empty response" $ do - let expected = C.pack "" - (TestHandle _ 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 "stops when the server hangs up during a multiline response" $ do + let + expected = + FTPResponse + F.Success + 220 + (MultiLine [C.pack "First Line"]) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + ] + Clear + getResponse h `shouldReturn` expected + describe "Network.FTP.Client.recvAll" $ + it "doesn't hang on empty response" $ do + let + expected = C.pack "" + (TestHandle _ h) <- testHandle [C.pack ""] [] Clear + recvAll h `shouldReturn` expected isBadProtocolResponse :: FTPException -> Bool isBadProtocolResponse e = - case e of - BadProtocolResponseException _ -> True - _ -> False + case e of + BadProtocolResponseException _ -> True + _ -> False diff --git a/scripts/format-repo b/scripts/format-repo new file mode 100755 index 0000000..cb84d20 --- /dev/null +++ b/scripts/format-repo @@ -0,0 +1,16 @@ +#!/bin/sh + +set -o errexit + +. scripts/lib/run-in-container.sh + +if [ "$CI" ]; then + # The container runs as root against a bind mounted checkout owned by the + # runner, so git refuses to read it without this. + git config --global --add safe.directory "$PWD" + MODE=check +else + MODE=inplace +fi + +git ls-files -z '*.hs' | xargs -0 fourmolu --mode "$MODE" diff --git a/stack-ghc-9.10.yaml b/stack-ghc-9.10.yaml index ef17196..cea3040 100644 --- a/stack-ghc-9.10.yaml +++ b/stack-ghc-9.10.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: lts-24.12 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.10.yaml.lock b/stack-ghc-9.10.yaml.lock index 6e6c820..c8c5d88 100644 --- a/stack-ghc-9.10.yaml.lock +++ b/stack-ghc-9.10.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 0d0bb681dd5be9b930c8fc070d717aae757b9aed176ae6047d87624b46406816 diff --git a/stack-ghc-9.12.yaml b/stack-ghc-9.12.yaml index ea4b986..3a21c92 100644 --- a/stack-ghc-9.12.yaml +++ b/stack-ghc-9.12.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: nightly-2026-08-16 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.12.yaml.lock b/stack-ghc-9.12.yaml.lock index 2f96d08..c796b54 100644 --- a/stack-ghc-9.12.yaml.lock +++ b/stack-ghc-9.12.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 38576406e65852b2665b3006437e74dfdb0a97977409b1c69de0986868f8c5b1 diff --git a/stack-ghc-9.6.yaml b/stack-ghc-9.6.yaml index 28649ec..59bf93a 100644 --- a/stack-ghc-9.6.yaml +++ b/stack-ghc-9.6.yaml @@ -1,4 +1,11 @@ <<: !include stack-base.yaml resolver: lts-22.44 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 + # lts-22 additionally lacks henforcer's config parser and its dependency + - tomland-1.3.3.3 + - validation-selective-0.2.0.0 diff --git a/stack-ghc-9.6.yaml.lock b/stack-ghc-9.6.yaml.lock index 8d134eb..2553e3d 100644 --- a/stack-ghc-9.6.yaml.lock +++ b/stack-ghc-9.6.yaml.lock @@ -3,7 +3,35 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 +- completed: + hackage: tomland-1.3.3.3@sha256:459e5bf268552e5e582e2d3df895dc41db618bb6306576d536b600cc5729de8f,9404 + pantry-tree: + sha256: 0caf0f2c5f19c16625556b049285a02850912569a4bc26831c04b3a1e388bc67 + size: 6430 + original: + hackage: tomland-1.3.3.3 +- completed: + hackage: validation-selective-0.2.0.0@sha256:a88df9c19a26509fc4b8098b2e110336efda7e4bc0389dd58bcf327e1726005e,3917 + pantry-tree: + sha256: 82a8a83073ac0a360b72c27a8fb6952b4566a20d1ea389b65f738303423177da + size: 697 + original: + hackage: validation-selective-0.2.0.0 snapshots: - completed: sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9 diff --git a/stack-ghc-9.8.yaml b/stack-ghc-9.8.yaml index 34d7f18..b2f24d7 100644 --- a/stack-ghc-9.8.yaml +++ b/stack-ghc-9.8.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: lts-23.28 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.8.yaml.lock b/stack-ghc-9.8.yaml.lock index a5db08c..6db565e 100644 --- a/stack-ghc-9.8.yaml.lock +++ b/stack-ghc-9.8.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 7e724f347d5969cb5e8dde9f9aae30996e3231c29d1dafd45f21f1700d4c4fcb diff --git a/stack.yaml.lock b/stack.yaml.lock index 6e6c820..c8c5d88 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 0d0bb681dd5be9b930c8fc070d717aae757b9aed176ae6047d87624b46406816