module Network.FTP.Client (
withFTP,
withFTPS,
login,
pasv,
rename,
dele,
cwd,
size,
mkd,
rmd,
pwd,
quit,
nlst,
retr,
list,
stor,
FTPCommand(..),
FTPResponse(..),
ResponseStatus(..),
RTypeCode(..),
PortActivity(..),
ProtType(..),
Security(..),
Handle(..),
sIOHandleImpl,
tlsHandleImpl,
sendCommand,
sendCommands,
getLineResp,
getMultiLineResp,
sendCommandLine,
createSendDataCommand,
createTLSSendDataCommand
) where
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 ((<>), mconcat)
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 System.IO.Error
import Data.ByteString.Lazy.Internal (defaultChunkSize)
import Data.Functor ((<$>))
import Control.Applicative ((<*>))
debugging :: Bool
debugging = False
debugPrint :: (Show a, MonadIO m) => a -> m ()
debugPrint s = debugPrint' s debugging
where
debugPrint' _ False = return ()
debugPrint' s True = liftIO $ print s
data Security = Clear | TLS
data Handle = Handle
{ send :: ByteString -> IO ()
, sendLine :: ByteString -> IO ()
, recv :: Int -> IO ByteString
, recvLine :: IO ByteString
, security :: Security
}
data FTPResponse = FTPResponse {
frStatus :: ResponseStatus,
frCode :: Int,
frMessage :: ByteString
}
instance Show FTPResponse where
show fr = (show $ frCode fr) <> " " <> (C.unpack $ frMessage fr)
data ResponseStatus
= Wait
| Success
| Continue
| FailureRetry
| Failure
deriving (Show)
responseStatus :: ByteString -> ResponseStatus
responseStatus cbs =
case C.uncons cbs of
Just ('1', _) -> Wait
Just ('2', _) -> Success
Just ('3', _) -> Continue
Just ('4', _) -> FailureRetry
_ -> Failure
data RTypeCode = TA | TI
serialzeRTypeCode :: RTypeCode -> String
serialzeRTypeCode TA = "A"
serialzeRTypeCode TI = "I"
data PortActivity = Active | Passive
data ProtType = P | C
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
| 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 " <> intercalate " " args
serializeCommand (Port ha pn) = "PORT " <> formatPort ha pn
serializeCommand (Stor loc) = "STOR " <> loc
serializeCommand (List []) = "LIST"
serializeCommand (List args) = "LIST " <> intercalate " " 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 (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')
getLineResp :: Handle -> IO ByteString
getLineResp h = stripCLRF <$> recvLine h
getMultiLineResp :: MonadIO m => Handle -> m FTPResponse
getMultiLineResp h = do
line <- liftIO $ getLineResp h
let (code, rest) = C.splitAt 3 line
message <- if C.head rest == '-'
then loopMultiLine h code line
else return line
return $ FTPResponse
(responseStatus code)
(read $ C.unpack code)
(C.drop 4 message)
loopMultiLine
:: MonadIO m
=> Handle
-> ByteString
-> ByteString
-> m ByteString
loopMultiLine h code line = do
nextLine <- liftIO $ getLineResp h
let multiLine = line <> "\n" <> nextLine
nextCode = C.take 3 nextLine
if nextCode == code
then return multiLine
else loopMultiLine h nextCode multiLine
sendCommandLine :: MonadIO m => Handle -> ByteString -> m ()
sendCommandLine h = liftIO . send h . (<> "\r\n")
sendCommand :: MonadIO m => Handle -> FTPCommand -> m FTPResponse
sendCommand h fc = do
let command = serializeCommand fc
debugPrint $ "Sending: " <> command
sendCommandLine h $ C.pack command
resp <- getMultiLineResp h
debugPrint $ "Recieved: " <> (show resp)
return resp
sendCommands :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]
sendCommands = mapM . sendCommand
createSocket :: MonadIO m => Maybe String -> Int -> S.AddrInfo -> m (S.Socket, S.AddrInfo)
createSocket host portNum hints = do
addr:_ <- liftIO $ S.getAddrInfo (Just hints) host (Just $ show portNum)
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
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
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)
withFTP
:: (MonadIO m, MonadMask m)
=> String
-> Int
-> (Handle -> FTPResponse -> m a)
-> m a
withFTP host portNum f = withSIOHandle host portNum $ \h -> do
resp <- getMultiLineResp h
f h resp
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
(S.SockAddrInet sPort sHost) <- liftIO $ S.getSocketName socket
port h sHost sPort
f socket
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
createSendDataCommand
:: (MonadIO m, MonadMask m)
=> Handle
-> PortActivity
-> [FTPCommand]
-> m (SIO.Handle)
createSendDataCommand h pa cmds = withDataSocket pa h $ \socket -> do
sendCommands h cmds
acceptedSock <- acceptData pa socket
liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode
withDataCommand
:: (MonadIO m, MonadMask m)
=> Handle
-> PortActivity
-> [FTPCommand]
-> (Handle -> m a)
-> m a
withDataCommand ch pa cmds f = do
x <- M.bracket
(createSendDataCommand ch pa cmds)
(liftIO . SIO.hClose)
(f . sIOHandleImpl)
resp <- getMultiLineResp ch
debugPrint $ "Recieved: " <> (show resp)
return x
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)
recvAll :: (MonadIO m, MonadCatch m) => Handle -> m ByteString
recvAll h = recvAll' ""
where
recvAll' bs = (do
chunk <- liftIO $ recv h defaultChunkSize
recvAll' $ bs <> chunk)
`M.catchIOError` (\_ -> return bs)
connectTLS :: MonadIO m => SIO.Handle -> String -> Int -> m Connection
connectTLS h host portNum = do
context <- liftIO initConnectionContext
let tlsSettings = TLSSettingsSimple
{ settingDisableCertificateValidation = True
, settingDisableSession = False
, settingUseServerName = False
}
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 <- getMultiLineResp 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)
withFTPS
:: (MonadMask m, MonadIO m)
=> String
-> Int
-> (Handle -> FTPResponse -> m a)
-> m a
withFTPS host portNum = withTLSHandle host portNum
createTLSSendDataCommand
:: (MonadIO m, MonadMask m)
=> Handle
-> PortActivity
-> [FTPCommand]
-> m Connection
createTLSSendDataCommand ch pa cmds = do
sendCommands ch [Pbsz 0, Prot P]
withDataSocket pa ch $ \socket -> do
sendCommands ch cmds
acceptedSock <- acceptData pa socket
(S.SockAddrInet sPort sHost) <- liftIO $ S.getSocketName acceptedSock
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
-> [FTPCommand]
-> (Handle -> m a)
-> m a
withTLSDataCommand ch pa cmds f = do
x <- M.bracket
(createTLSSendDataCommand ch pa cmds)
(liftIO . connectionClose)
(f . tlsHandleImpl)
resp <- getMultiLineResp ch
debugPrint $ "Recieved: " <> (show resp)
return x
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 (== '"')
login :: MonadIO m => Handle -> String -> String -> m FTPResponse
login h user pass = last <$> sendCommands h [User user, Pass pass]
pasv :: MonadIO m => Handle -> m (String, Int)
pasv h = do
resp <- sendCommand h Pasv
let (Right (host, portNum)) = parseOnly parse227 (frMessage resp)
return (host, portNum)
port :: MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse
port h ha pn = sendCommand h (Port ha pn)
acct :: MonadIO m => Handle -> String -> m FTPResponse
acct h pass = sendCommand 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 -> sendCommand h (Rnto to)
_ -> return res
dele :: MonadIO m => Handle -> String -> m FTPResponse
dele h file = sendCommand h (Dele file)
cwd :: MonadIO m => Handle -> String -> m FTPResponse
cwd h dir =
sendCommand h $ if dir == ".."
then Cdup
else Cwd dir
size :: MonadIO m => Handle -> String -> m Int
size h file = do
resp <- sendCommand h (Size file)
return $ read $ C.unpack $ frMessage resp
mkd :: MonadIO m => Handle -> String -> m String
mkd h dir = do
resp <- sendCommand h (Mkd dir)
let (Right dir) = parseOnly parse257 (frMessage resp)
return dir
rmd :: MonadIO m => Handle -> String -> m FTPResponse
rmd h dir = sendCommand h (Rmd dir)
pwd :: MonadIO m => Handle -> m String
pwd h = do
resp <- sendCommand h Pwd
let (Right dir) = parseOnly parse257 (frMessage resp)
return dir
quit :: MonadIO m => Handle -> m FTPResponse
quit h = sendCommand h Quit
pbsz :: MonadIO m => Handle -> Int -> m FTPResponse
pbsz h = sendCommand h . Pbsz
prot :: MonadIO m => Handle -> ProtType -> m FTPResponse
prot h = sendCommand h . Prot
ccc :: MonadIO m => Handle -> m FTPResponse
ccc h = sendCommand h Ccc
auth :: MonadIO m => Handle -> m FTPResponse
auth h = sendCommand h Auth
sendType :: MonadIO m => RTypeCode -> ByteString -> Handle -> m ()
sendType TA dat h = void $ mapM (sendCommandLine h) $ C.split '\n' dat
sendType TI dat h = liftIO $ send h dat
withDataCommandSecurity
:: (MonadIO m, MonadMask m)
=> Handle
-> PortActivity
-> [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 [RType TA, Nlst args] getAllLineResp
retr :: (MonadIO m, MonadMask m) => Handle -> String -> m ByteString
retr h path = withDataCommandSecurity h Passive [RType TI, Retr path] recvAll
list :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString
list h args = withDataCommandSecurity h Passive [RType TA, List args] recvAll
stor
:: (MonadIO m, MonadMask m)
=> Handle
-> String
-> B.ByteString
-> RTypeCode
-> m ()
stor h loc dat rtype =
withDataCommandSecurity h Passive [RType rtype, Stor loc]
$ sendType rtype dat