irc-dcc 1.2.1 → 2.0.0
raw patch · 11 files changed
+756/−660 lines, 11 filesdep +irc-dccdep +mtldep +safe-exceptionsdep ~basedep ~binarydep ~transformersPVP ok
version bump matches the API change (PVP)
Dependencies added: irc-dcc, mtl, safe-exceptions, tasty-quickcheck
Dependency ranges changed: base, binary, transformers
API changes (from Hackage documentation)
- Network.IRC.DCC: AcceptResumeFile :: TransferType -> FileMetadata -> FileOffset -> AcceptResumeFile
- Network.IRC.DCC: Active :: IPv4 -> PortNumber -> TransferType
- Network.IRC.DCC: CloseChat :: CloseChat
- Network.IRC.DCC: FileMetadata :: Path Rel File -> Maybe FileOffset -> FileMetadata
- Network.IRC.DCC: FileTransfer :: OfferFile -> Service
- Network.IRC.DCC: Messaging :: OpenChat -> Service
- Network.IRC.DCC: OfferFile :: TransferType -> FileMetadata -> OfferFile
- Network.IRC.DCC: OfferFileSink :: Token -> FileMetadata -> IPv4 -> PortNumber -> OfferFileSink
- Network.IRC.DCC: Passive :: IPv4 -> Token -> TransferType
- Network.IRC.DCC: TryResumeFile :: TransferType -> FileMetadata -> FileOffset -> TryResumeFile
- Network.IRC.DCC: Whiteboard :: IPv4 -> PortNumber -> OpenChat
- Network.IRC.DCC: [fileName] :: FileMetadata -> Path Rel File
- Network.IRC.DCC: [fileSize] :: FileMetadata -> Maybe FileOffset
- Network.IRC.DCC: data AcceptResumeFile
- Network.IRC.DCC: data CloseChat
- Network.IRC.DCC: data FileMetadata
- Network.IRC.DCC: data OfferFile
- Network.IRC.DCC: data OfferFileSink
- Network.IRC.DCC: data OpenChat
- Network.IRC.DCC: data Service
- Network.IRC.DCC: data TransferType
- Network.IRC.DCC: data TryResumeFile
- Network.IRC.DCC: encodeCtcp :: CtcpCommand a => a -> CTCPByteString
- Network.IRC.DCC: parseAcceptResumeFile :: TryResumeFile -> Parser AcceptResumeFile
- Network.IRC.DCC: parseCloseChat :: Parser CloseChat
- Network.IRC.DCC: parseOfferFile :: Parser OfferFile
- Network.IRC.DCC: parseOfferFileSink :: AcceptResumeFile -> Parser (Maybe OfferFileSink)
- Network.IRC.DCC: parseOpenChat :: Parser OpenChat
- Network.IRC.DCC: parseService :: Parser Service
- Network.IRC.DCC: parseTryResumeFile :: OfferFile -> Parser TryResumeFile
- Network.IRC.DCC: runParser :: Parser a -> CTCPByteString -> Either String a
- Network.IRC.DCC.FileTransfer: acceptFile :: Integral a => OfferFile -> (PortNumber -> ExceptT String IO ()) -> (a -> IO ()) -> ReaderT (Maybe PortNumber) (ExceptT String IO) ()
- Network.IRC.DCC.FileTransfer: resumeFile :: Integral a => AcceptResumeFile -> (PortNumber -> ExceptT String IO ()) -> (a -> IO ()) -> ReaderT (Maybe PortNumber) (ExceptT String IO) ()
+ Network.IRC.DCC: Abs :: PathType -> (Path Abs File) -> Path
+ Network.IRC.DCC: Accept :: !Path -> !PortNumber -> !FileOffset -> DccAccept
+ Network.IRC.DCC: AcceptReverse :: !Path -> !FileOffset -> !Token -> DccAccept
+ Network.IRC.DCC: ChatWhiteboard :: !IPv4 -> !PortNumber -> DccChat
+ Network.IRC.DCC: Close :: DccClose
+ Network.IRC.DCC: Quoted :: PathType
+ Network.IRC.DCC: Rel :: PathType -> (Path Rel File) -> Path
+ Network.IRC.DCC: Resume :: !Path -> !PortNumber -> !FileOffset -> DccResume
+ Network.IRC.DCC: ResumeReverse :: !Path -> !FileOffset -> !Token -> DccResume
+ Network.IRC.DCC: Send :: !Path -> !IPv4 -> !PortNumber -> !(Maybe FileOffset) -> DccSend
+ Network.IRC.DCC: SendReverseClient :: !Path -> !IPv4 -> !PortNumber -> !FileOffset -> !Token -> DccSendReverseClient
+ Network.IRC.DCC: SendReverseServer :: !Path -> !IPv4 -> !FileOffset -> !Token -> DccSend
+ Network.IRC.DCC: Simple :: PathType
+ Network.IRC.DCC: acceptedPosition :: DccAccept -> FileOffset
+ Network.IRC.DCC: data DccAccept
+ Network.IRC.DCC: data DccChat
+ Network.IRC.DCC: data DccClose
+ Network.IRC.DCC: data DccResume
+ Network.IRC.DCC: data DccSend
+ Network.IRC.DCC: data DccSendReverseClient
+ Network.IRC.DCC: data Path
+ Network.IRC.DCC: data PathType
+ Network.IRC.DCC: fromCtcp :: CtcpCommand a => CTCPByteString -> Either String a
+ Network.IRC.DCC: fromPath :: Path -> Path Rel File
+ Network.IRC.DCC: matchesSend :: DccAccept -> DccSend -> Bool
+ Network.IRC.DCC: resumeFromSend :: DccSend -> FileOffset -> DccResume
+ Network.IRC.DCC: toCtcp :: CtcpCommand a => a -> CTCPByteString
+ Network.IRC.DCC.Client.FileTransfer: Active :: !IPv4 -> !PortNumber -> (m ()) -> ConnectionType m
+ Network.IRC.DCC.Client.FileTransfer: Download :: !(Path Rel File) -> !(ConnectionType m) -> !TransferType -> (FileOffset -> m ()) -> FileTransfer m
+ Network.IRC.DCC.Client.FileTransfer: FromStart :: TransferType
+ Network.IRC.DCC.Client.FileTransfer: Passive :: !IPv4 -> !(Maybe PortNumber) -> (PortNumber -> m ()) -> ConnectionType m
+ Network.IRC.DCC.Client.FileTransfer: ResumeFrom :: !FileOffset -> TransferType
+ Network.IRC.DCC.Client.FileTransfer: [_connectionType] :: FileTransfer m -> !(ConnectionType m)
+ Network.IRC.DCC.Client.FileTransfer: [_fileName] :: FileTransfer m -> !(Path Rel File)
+ Network.IRC.DCC.Client.FileTransfer: [_onChunk] :: FileTransfer m -> FileOffset -> m ()
+ Network.IRC.DCC.Client.FileTransfer: [_transferType] :: FileTransfer m -> !TransferType
+ Network.IRC.DCC.Client.FileTransfer: acceptFile :: DccSend -> (PortNumber -> IO ()) -> (FileOffset -> IO ()) -> ReaderT (Maybe PortNumber) IO ()
+ Network.IRC.DCC.Client.FileTransfer: data ConnectionType m
+ Network.IRC.DCC.Client.FileTransfer: data FileTransfer m
+ Network.IRC.DCC.Client.FileTransfer: data TransferType
+ Network.IRC.DCC.Client.FileTransfer: resumeFile :: DccSend -> DccAccept -> (PortNumber -> IO ()) -> (FileOffset -> IO ()) -> ReaderT (Maybe PortNumber) IO ()
+ Network.IRC.DCC.Client.FileTransfer: transfer :: (MonadMask m, MonadIO m) => FileTransfer m -> m ()
+ Network.IRC.DCC.Internal: Abs :: PathType -> (Path Abs File) -> Path
+ Network.IRC.DCC.Internal: Accept :: !Path -> !PortNumber -> !FileOffset -> DccAccept
+ Network.IRC.DCC.Internal: AcceptReverse :: !Path -> !FileOffset -> !Token -> DccAccept
+ Network.IRC.DCC.Internal: Chat :: !IPv4 -> !PortNumber -> DccChat
+ Network.IRC.DCC.Internal: ChatWhiteboard :: !IPv4 -> !PortNumber -> DccChat
+ Network.IRC.DCC.Internal: Close :: DccClose
+ Network.IRC.DCC.Internal: FileOffset :: Word64 -> FileOffset
+ Network.IRC.DCC.Internal: Quoted :: PathType
+ Network.IRC.DCC.Internal: Rel :: PathType -> (Path Rel File) -> Path
+ Network.IRC.DCC.Internal: Resume :: !Path -> !PortNumber -> !FileOffset -> DccResume
+ Network.IRC.DCC.Internal: ResumeReverse :: !Path -> !FileOffset -> !Token -> DccResume
+ Network.IRC.DCC.Internal: Send :: !Path -> !IPv4 -> !PortNumber -> !(Maybe FileOffset) -> DccSend
+ Network.IRC.DCC.Internal: SendReverseClient :: !Path -> !IPv4 -> !PortNumber -> !FileOffset -> !Token -> DccSendReverseClient
+ Network.IRC.DCC.Internal: SendReverseServer :: !Path -> !IPv4 -> !FileOffset -> !Token -> DccSend
+ Network.IRC.DCC.Internal: Simple :: PathType
+ Network.IRC.DCC.Internal: Token :: ByteString -> Token
+ Network.IRC.DCC.Internal: [toWord] :: FileOffset -> Word64
+ Network.IRC.DCC.Internal: acceptedPosition :: DccAccept -> FileOffset
+ Network.IRC.DCC.Internal: class CtcpCommand a
+ Network.IRC.DCC.Internal: data DccAccept
+ Network.IRC.DCC.Internal: data DccChat
+ Network.IRC.DCC.Internal: data DccClose
+ Network.IRC.DCC.Internal: data DccResume
+ Network.IRC.DCC.Internal: data DccSend
+ Network.IRC.DCC.Internal: data DccSendReverseClient
+ Network.IRC.DCC.Internal: data Path
+ Network.IRC.DCC.Internal: data PathType
+ Network.IRC.DCC.Internal: decimalInRange :: (Integer, Integer) -> Parser Integer
+ Network.IRC.DCC.Internal: fileOffset :: Parser FileOffset
+ Network.IRC.DCC.Internal: fileOffsetToBS :: FileOffset -> ByteString
+ Network.IRC.DCC.Internal: fromBigEndianIp :: Integer -> IPv4
+ Network.IRC.DCC.Internal: fromCtcp :: CtcpCommand a => CTCPByteString -> Either String a
+ Network.IRC.DCC.Internal: fromPath :: Path -> Path Rel File
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccAccept
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccChat
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccClose
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccResume
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccSend
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.DccSendReverseClient
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.Path
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.PathType
+ Network.IRC.DCC.Internal: instance GHC.Classes.Eq Network.IRC.DCC.Internal.Token
+ Network.IRC.DCC.Internal: instance GHC.Classes.Ord Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Enum.Bounded Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Enum.Enum Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Num.Num Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Real.Integral Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Real.Real Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccAccept
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccChat
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccClose
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccResume
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccSend
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.DccSendReverseClient
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.FileOffset
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.Path
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.PathType
+ Network.IRC.DCC.Internal: instance GHC.Show.Show Network.IRC.DCC.Internal.Token
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccAccept
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccChat
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccClose
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccResume
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccSend
+ Network.IRC.DCC.Internal: instance Network.IRC.DCC.Internal.CtcpCommand Network.IRC.DCC.Internal.DccSendReverseClient
+ Network.IRC.DCC.Internal: ipBigEndian :: Parser IPv4
+ Network.IRC.DCC.Internal: ipToBigEndianBS :: IPv4 -> ByteString
+ Network.IRC.DCC.Internal: newtype FileOffset
+ Network.IRC.DCC.Internal: newtype Token
+ Network.IRC.DCC.Internal: parseCtcp :: Parser a -> CTCPByteString -> Either String a
+ Network.IRC.DCC.Internal: path :: Parser Path
+ Network.IRC.DCC.Internal: pathToBS :: Path -> ByteString
+ Network.IRC.DCC.Internal: socket :: Parser (IPv4, PortNumber)
+ Network.IRC.DCC.Internal: socketToBS :: (IPv4, PortNumber) -> ByteString
+ Network.IRC.DCC.Internal: spaced :: Parser a -> Parser a
+ Network.IRC.DCC.Internal: tcpPort :: Parser PortNumber
+ Network.IRC.DCC.Internal: tcpPortToBS :: PortNumber -> ByteString
+ Network.IRC.DCC.Internal: toBigEndianIp :: IPv4 -> Integer
+ Network.IRC.DCC.Internal: toCtcp :: CtcpCommand a => a -> CTCPByteString
+ Network.IRC.DCC.Internal: token :: Parser Token
+ Network.IRC.DCC.Internal: tokenToBS :: Token -> ByteString
+ Network.IRC.DCC.Internal: wrap :: PathType -> ByteString -> ByteString
- Network.IRC.DCC: Chat :: IPv4 -> PortNumber -> OpenChat
+ Network.IRC.DCC: Chat :: !IPv4 -> !PortNumber -> DccChat
Files
- irc-dcc.cabal +60/−58
- src/Network/IRC/DCC.hs +41/−27
- src/Network/IRC/DCC/Client/FileTransfer.hs +136/−0
- src/Network/IRC/DCC/FileTransfer.hs +0/−85
- src/Network/IRC/DCC/Internal.hs +249/−306
- src/Network/Socket/ByteString/Extended.hs +67/−59
- src/System/IO/Streams/Lifted.hs +32/−0
- test/Network/IRC/DCCTest.hs +166/−0
- test/Spec.hs +5/−0
- tests/Main.hs +0/−7
- tests/Network/IRC/DCCTest.hs +0/−118
irc-dcc.cabal view
@@ -1,65 +1,67 @@-name: irc-dcc-version: 1.2.1-synopsis: A DCC message parsing and helper library for IRC clients-description: DCC (Direct Client-to-Client) is an IRC sub-protocol for- establishing and maintaining direct connections to- exchange messages and files.- .- See <http://www.irchelp.org/irchelp/rfc/ctcpspec.html> for- more details.-license: MIT-license-file: LICENSE-homepage: https://github.com/JanGe/irc-dcc-bug-reports: https://github.com/JanGe/irc-dcc/issues-author: Jan Gerlinger-maintainer: git@jangerlinger.de-category: Network--- copyright:-build-type: Simple-cabal-version: >= 1.10+name: irc-dcc+version: 2.0.0+synopsis: A DCC message parsing and helper library for IRC clients+description: DCC (Direct Client-to-Client) is an IRC sub-protocol for+ establishing and maintaining direct connections to+ exchange messages and files.+ .+ See <http://www.irchelp.org/irchelp/rfc/ctcpspec.html> for+ more details.+license: MIT+license-file: LICENSE+homepage: https://github.com/JanGe/irc-dcc+bug-reports: https://github.com/JanGe/irc-dcc/issues+author: Jan Gerlinger+maintainer: git@jangerlinger.de+category: Network+copyright: 2016 Jan Gerlinger+build-type: Simple+cabal-version: >= 1.10 library- hs-source-dirs: src- exposed-modules: Network.IRC.DCC- , Network.IRC.DCC.FileTransfer- other-modules: Network.IRC.DCC.Internal- , Network.Socket.ByteString.Extended+ hs-source-dirs: src+ exposed-modules: Network.IRC.DCC+ , Network.IRC.DCC.Internal+ , Network.IRC.DCC.Client.FileTransfer+ other-modules: Network.Socket.ByteString.Extended+ , System.IO.Streams.Lifted -- other-extensions:- build-depends: base >= 4.7 && < 5- , attoparsec >= 0.13.0.1 && < 0.14- , binary >= 0.7.5.0- , bytestring >= 0.10.6.0 && < 0.11- , errors >= 2.1.2 && < 2.2- , io-streams >= 1.3.5.0 && < 1.4- , iproute >= 1.7.0 && < 1.8- , irc-ctcp >= 0.1.3.0 && < 0.2- , network >= 2.6.2.1 && < 2.7- , path >= 0.5.7 && < 0.6- , transformers >= 0.4.2.0- , utf8-string >= 1.0.1.1 && < 1.1- default-language: Haskell2010- ghc-options: -Wall -fno-warn-unused-do-bind+ build-depends: base >= 4.8 && < 5+ , attoparsec >= 0.13.0.1 && < 0.14+ , binary >= 0.7.5.0 && < 0.9+ , bytestring >= 0.10.6.0 && < 0.11+ , errors >= 2.1.2 && < 2.2+ , io-streams >= 1.3.5.0 && < 1.4+ , iproute >= 1.7.0 && < 1.8+ , irc-ctcp >= 0.1.3.0 && < 0.2+ , mtl >= 2.2.1 && < 2.3+ , network >= 2.6.2.1 && < 2.7+ , path >= 0.5.7 && < 0.6+ , safe-exceptions >= 0.1.2.0 && < 0.2+ , transformers >= 0.4.2.0 && < 0.6+ , utf8-string >= 1.0.1.1 && < 1.1+ default-language: Haskell2010+ ghc-options: -Wall -fno-warn-unused-do-bind -test-suite tests- hs-source-dirs: tests- , src- main-is: Main.hs- other-modules: Network.IRC.DCCTest- , Network.IRC.DCC.Internal- type: exitcode-stdio-1.0- build-depends: base >= 4.7 && < 5- , tasty >= 0.11.0.2 && < 0.12- , tasty-hspec >= 1.1.2 && < 1.2- , hspec-attoparsec >= 0.1.0.2 && < 0.2- , attoparsec >= 0.13.0.1 && < 0.14- , binary >= 0.7.5.0- , bytestring >= 0.10.6.0 && < 0.11- , iproute >= 1.7.0 && < 1.8- , irc-ctcp >= 0.1.3.0 && < 0.2- , network >= 2.6.2.1 && < 2.7- , path >= 0.5.7 && < 0.6- , utf8-string >= 1.0.1.1 && < 1.1- default-language: Haskell2010+test-suite irc-dcc-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules: Network.IRC.DCCTest+ build-depends: base >= 4.8 && < 5+ , bytestring >= 0.10.6.0 && < 0.11+ , hspec-attoparsec >= 0.1.0.2 && < 0.2+ , iproute >= 1.7.0 && < 1.8+ , irc-dcc+ , network >= 2.6.2.1 && < 2.7+ , path >= 0.5.7 && < 0.6+ , utf8-string >= 1.0.1.1 && < 1.1+ , tasty >= 0.11.0.2 && < 0.12+ , tasty-hspec >= 1.1.2 && < 1.2+ , tasty-quickcheck >= 0.8.4 && < 0.9+ ghc-options: -Wall -fno-warn-orphans+ -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010 source-repository head type: git
src/Network/IRC/DCC.hs view
@@ -1,40 +1,54 @@ {-| DCC command parsing and encoding module. - Example of parsing an offer file command:+ Use the 'CtcpCommand' type class to convert between 'CTCPByteString's+ and typed values. - > runParser parseOfferFile ctcpMessage+ Try converting a 'CTCPByteString' to a 'DccSend' value: - Example of encoding an offer file command:+ > fromCtcp ctcpMessage :: Either String DccSend - > encodeCtcp offerFile+ Encoding a 'DccSend' value to a 'CTCPByteString':++ > toCtcp (Send fileName ip port (Just fileSize)) -} module Network.IRC.DCC (- -- * Types- -- ** DCC service+ -- * DCC command parsing and encoding CtcpCommand(..)- , Service(..)+ -- * DCC command types -- ** Messaging commands (DCC CHAT)- , OpenChat(..)- , CloseChat(..)+ , DccChat(..)+ , DccClose(..) -- ** File Transfer commands (DCC SEND)- , OfferFile(..)- , TryResumeFile(..)- , AcceptResumeFile(..)- , OfferFileSink(..)- -- *** Helper Types- , TransferType(..)- , FileMetadata(..)- , Token(..)+ , DccSend(..)+ , DccResume(..)+ , DccAccept(..)+ , acceptedPosition+ , DccSendReverseClient(..)+ -- *** Constructors from other commands+ , resumeFromSend+ -- *** Protocol variant checks+ , matchesSend+ -- ** Helper Types+ , Path(..)+ , fromPath+ , PathType(..) , FileOffset- -- * DCC command parsing- , runParser- , parseService- , parseOpenChat- , parseCloseChat- , parseOfferFile- , parseTryResumeFile- , parseAcceptResumeFile- , parseOfferFileSink+ , Token(..) ) where -import Network.IRC.DCC.Internal+import Network.IRC.DCC.Internal++-- | Try resuming a file offer+resumeFromSend :: DccSend -> FileOffset -> DccResume+resumeFromSend (Send path' _ port _) pos =+ Resume path' port pos+resumeFromSend (SendReverseServer path' _ _ token') pos =+ ResumeReverse path' pos token'++-- | Check if a 'DccSend' and a 'DccAccept' command are part of the same negotiation.+matchesSend :: DccAccept -> DccSend -> Bool+matchesSend (Accept pathA portA _) (Send pathS _ portS _) =+ pathS == pathA && portS == portA+matchesSend (AcceptReverse pathA _ tokenA) (SendReverseServer pathS _ _ tokenS) =+ pathS == pathA && tokenS == tokenA+matchesSend _ _ = False
+ src/Network/IRC/DCC/Client/FileTransfer.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE RecordWildCards #-}++{-| Functions for receiving files.++ Each chunk is acknowledged by sending the total sum of bytes received for a+ file. See the+ <http://www.irchelp.org/irchelp/rfc/ctcpspec.html CTCP specification>.+-}+module Network.IRC.DCC.Client.FileTransfer (+ -- * Start/resume DCC file transfer+ acceptFile+ , resumeFile+ -- * Custom DCC file transfer+ , FileTransfer(..)+ , ConnectionType(..)+ , TransferType(..)+ , transfer+ ) where++import Network.IRC.DCC++import Control.Exception.Safe+import Control.Monad (unless)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT, ask)+import Data.ByteString.Char8 (ByteString, length, null)+import Network.Socket.ByteString.Extended (ConnectionType (..),+ PortNumber, Socket, close,+ connect, recv, sendAll,+ toNetworkByteOrder)+import qualified Path as P (File, Path, Rel,+ fromRelFile)+import Prelude hiding (length, null)+import System.IO (BufferMode (NoBuffering), IOMode (AppendMode, WriteMode))+import System.IO.Streams (OutputStream, write)+import System.IO.Streams.Lifted (withFileAsOutputExt)++data TransferType = FromStart+ | ResumeFrom !FileOffset++-- | A description of a file transfer.+-- You can specify a callback that will be called with the number of bytes+-- transfered for each chunk.+data FileTransfer m = Download { _fileName :: !(P.Path P.Rel P.File)+ , _connectionType :: !(ConnectionType m)+ , _transferType :: !TransferType+ , _onChunk :: FileOffset -> m ()+ }++-- | Accept a DCC file offer+acceptFile :: DccSend+ -> (PortNumber -> IO ())+ -- ^ Callback when socket is ready+ -> (FileOffset -> IO ())+ -- ^ Callback when a chunk of data was transfered+ -> ReaderT (Maybe PortNumber) IO ()+acceptFile = download FromStart++-- | Accept a DCC file offer for a partially downloaded file+resumeFile :: DccSend+ -> DccAccept+ -> (PortNumber -> IO ())+ -- ^ Callback when socket is ready+ -> (FileOffset -> IO ())+ -- ^ Callback when a chunk of data was transferred+ -> ReaderT (Maybe PortNumber) IO ()+resumeFile offer accept+ | accept `matchesSend` offer =+ download (ResumeFrom pos) offer+ | otherwise = fail "You mixed the DCC and Reverse DCC workflow when calling 'resumeFile'."+ where+ pos = acceptedPosition accept++download :: TransferType+ -> DccSend+ -> (PortNumber -> IO ())+ -> (FileOffset -> IO ())+ -> ReaderT (Maybe PortNumber) IO ()+download tt (Send path ip port _) onListen onChunk =+ lift $ transfer Download { _fileName = fromPath path+ , _connectionType = Active ip port (onListen port)+ , _transferType = tt+ , _onChunk = onChunk+ }+download tt (SendReverseServer path ip _ _) onListen onChunk = do+ port <- ask+ lift $ transfer Download { _fileName = fromPath path+ , _connectionType = Passive ip port onListen+ , _transferType = tt+ , _onChunk = onChunk+ }++transfer :: (MonadMask m, MonadIO m) => FileTransfer m -> m ()+transfer Download {..} =+ bracket (connect _connectionType)+ (liftIO . close)+ (streamToFile _fileName _transferType _onChunk)++streamToFile :: (MonadMask m, MonadIO m)+ => P.Path P.Rel P.File+ -> TransferType+ -> (FileOffset -> m ())+ -- ^ Callback when a chunk of data was transfered+ -> Socket+ -> m ()+streamToFile name tt onChunk =+ withFileAsOutputExt (P.fromRelFile name) (mode tt) NoBuffering .+ stream (pos tt) onChunk+ where+ mode FromStart = WriteMode+ mode (ResumeFrom _) = AppendMode+ pos FromStart = 0+ pos (ResumeFrom p) = p++stream :: (MonadMask m, MonadIO m)+ => FileOffset+ -> (FileOffset -> m ())+ -> Socket+ -> OutputStream ByteString+ -> m ()+stream pos onChunk sock h = do+ buf <- liftIO $ recv sock (4096 * 1024)+ unless (null buf) $ do+ let len = fromIntegral $ length buf+ onChunk len+ let pos' = pos + len+ sendPosition sock pos'+ liftIO $ Just buf `write` h+ stream pos' onChunk sock h++sendPosition :: (MonadMask m, MonadIO m)+ => Socket+ -> FileOffset+ -> m ()+sendPosition sock = liftIO . sendAll sock . toNetworkByteOrder
− src/Network/IRC/DCC/FileTransfer.hs
@@ -1,85 +0,0 @@-{-| Functions for receiving files.-- Each chunk is acknowledged by sending the total sum of bytes received for a- file. See the- <http://www.irchelp.org/irchelp/rfc/ctcpspec.html CTCP specification>.--}-module Network.IRC.DCC.FileTransfer- ( acceptFile- , resumeFile- ) where--import Network.IRC.DCC.Internal--import Control.Error-import Control.Monad (unless)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Reader (ReaderT, ask)-import Data.ByteString.Char8 (ByteString, length, null)-import Network.Socket.ByteString.Extended-import Path (File, Path, Rel,- fromRelFile)-import Prelude hiding (length, null)-import System.IO (BufferMode (NoBuffering), IOMode (AppendMode, WriteMode))-import System.IO.Streams (OutputStream,- withFileAsOutputExt, write)---- | Accept a DCC file offer-acceptFile :: Integral a- => OfferFile- -> (PortNumber -> ExceptT String IO ())- -- ^ Callback when socket is ready- -> (a -> IO ())- -- ^ Callback when a chunk of data was transfered- -> ReaderT (Maybe PortNumber) (ExceptT String IO) ()-acceptFile (OfferFile tt f) =- download (fileName f) WriteMode 0 tt---- | Accept a DCC file offer for a partially downloaded file-resumeFile :: Integral a- => AcceptResumeFile- -> (PortNumber -> ExceptT String IO ())- -- ^ Callback when socket is ready- -> (a -> IO ())- -- ^ Callback when a chunk of data was transfered- -> ReaderT (Maybe PortNumber) (ExceptT String IO) ()-resumeFile (AcceptResumeFile tt f pos) =- download (fileName f) AppendMode (fromIntegral pos) tt--download :: Integral a- => Path Rel File- -> IOMode- -> a- -> TransferType- -> (PortNumber -> ExceptT String IO ())- -- ^ Callback when socket is ready- -> (a -> IO ())- -- ^ Callback when a chunk of data was transfered- -> ReaderT (Maybe PortNumber) (ExceptT String IO) ()-download fn mode pos tt onListen onChunk = do- localPort <- ask- lift $- withSocket tt localPort onListen $- withFileAsOutputExt (fromRelFile fn) mode NoBuffering .- stream pos onChunk- where withSocket (Active i p) _ = withActiveSocket (Sink i p)- withSocket (Passive i _) p = withPassiveSocket (Source i p)--stream :: Integral a- => a- -> (a -> IO ())- -> Socket- -> OutputStream ByteString- -> IO ()-stream pos onChunk sock h =- do buf <- recv sock (4096 * 1024)- unless (null buf) $- do let l = fromIntegral (length buf)- onChunk l- let pos' = pos + l- sendPosition sock pos'- Just buf `write` h- stream pos' onChunk sock h--sendPosition :: Integral a => Socket -> a -> IO ()-sendPosition sock = sendAll sock . toNetworkByteOrder
src/Network/IRC/DCC/Internal.hs view
@@ -3,376 +3,319 @@ module Network.IRC.DCC.Internal where -import Control.Applicative-import Control.Monad (when)-import Data.Attoparsec.ByteString.Char8-import Data.Binary (byteSwap32)-import Data.ByteString.Char8 (ByteString, pack)-import qualified Data.ByteString.UTF8 as UTF8 (toString)-import Data.IP (IPv4, fromHostAddress,- toHostAddress)-import Data.Monoid ((<>))-import Data.Word (Word64)-import Network.IRC.CTCP (CTCPByteString, decodeCTCP,- encodeCTCP)-import Network.Socket (PortNumber)-import Path (File, Path, Rel, filename,- fromRelFile, parseAbsFile,- parseRelFile)+import Control.Applicative ((<|>))+import Control.Monad (when)+import Data.Attoparsec.ByteString.Char8 (Parser, choice, decimal,+ endOfInput, parseOnly,+ space, takeByteString,+ takeWhile1)+import Data.Binary (byteSwap32)+import Data.ByteString.Char8 (ByteString, pack, unwords)+import qualified Data.ByteString.UTF8 as UTF8 (fromString,+ toString)+import Data.IP (IPv4, fromHostAddress,+ toHostAddress)+import Data.Monoid ((<>))+import Data.Word (Word64)+import Network.IRC.CTCP (CTCPByteString, decodeCTCP,+ encodeCTCP)+import Network.Socket.ByteString.Extended (PortNumber)+import qualified Path as P (Abs, File, Path, Rel,+ filename, fromAbsFile,+ fromRelFile,+ parseAbsFile,+ parseRelFile)+import Prelude hiding (abs, unwords) --- | Type of DCC service offered-data Service- -- | Offer chat session- = Messaging OpenChat- -- | Offer file transfer- | FileTransfer OfferFile- deriving (Eq, Show)+-- | CTCP commands that can be parsed and encoded+class CtcpCommand a where+ toCtcp :: a -> CTCPByteString+ fromCtcp :: CTCPByteString -> Either String a --- | Type of DCC chat to open-data OpenChat+parseCtcp :: Parser a -> CTCPByteString -> Either String a+parseCtcp p = parseOnly (p <* endOfInput) . decodeCTCP++-- | Offer DCC chat session+data DccChat {-| Text messages exchange > DCC CHAT chat <ip> <port> -}- = Chat IPv4 PortNumber+ = Chat !IPv4 !PortNumber {-| Drawing commands exchange > DCC CHAT wboard <ip> <port> -}- | Whiteboard IPv4 PortNumber+ | ChatWhiteboard !IPv4 !PortNumber deriving (Eq, Show) +instance CtcpCommand DccChat where+ toCtcp (Chat ip port) = encodeCTCP $ unwords+ [ "DCC CHAT chat"+ , socketToBS (ip, port)+ ]+ toCtcp (ChatWhiteboard ip port) = encodeCTCP $ unwords+ [ "DCC CHAT wboard"+ , socketToBS (ip, port)+ ]++ fromCtcp = parseCtcp $+ Chat+ <$> ("DCC CHAT chat" *> spaced ipBigEndian)+ <*> spaced tcpPort+ <|> ChatWhiteboard+ <$> ("DCC CHAT wboard" *> spaced ipBigEndian)+ <*> spaced tcpPort+ -- | Signal intent to close DCC chat connection-data CloseChat+data DccClose -- | > DCC CLOSE- = CloseChat+ = Close deriving (Eq, Show) --- | DCC file transfer instructions-data OfferFile- {-| DCC:-- > DCC SEND <fileName> <ip> <port> (<fileSize>)+instance CtcpCommand DccClose where+ toCtcp Close = encodeCTCP "DCC CLOSE"+ fromCtcp = parseCtcp $ Close <$ "DCC CLOSE" - Reverse DCC:+-- | Offer DCC file transfer+data DccSend+ {-| As part of the standard DCC protocol, sent by the server - > DCC SEND <fileName> <ip> 0 <fileSize> <token>+ > DCC SEND <path> <ip> <port> (<fileSize>) -}- = OfferFile TransferType FileMetadata- deriving (Eq, Show)---- | Signal intent to resume DCC file transfer at specific position-data TryResumeFile- {-| DCC:-- > DCC RESUME <fileName> <port> <position>-- Reverse DCC:+ = Send !Path !IPv4 !PortNumber !(Maybe FileOffset)+ {-| As part of the Reverse DCC protocol, sent by the server - > DCC RESUME <fileName> 0 <position> <token>- -}- = TryResumeFile TransferType FileMetadata FileOffset+ > DCC SEND <path> <ip> 0 <fileSize> <token>+ -}+ | SendReverseServer !Path !IPv4 !FileOffset !Token deriving (Eq, Show) --- | Signal acceptance to resume DCC file transfer at specific position-data AcceptResumeFile- {-| DCC:+instance CtcpCommand DccSend where+ toCtcp (Send name ip port size) = encodeCTCP $ unwords+ [ "DCC SEND"+ , pathToBS name+ , ipToBigEndianBS ip+ , tcpPortToBS port ]+ <> maybe "" ((" " <>) . fileOffsetToBS) size+ toCtcp (SendReverseServer name ip size t) = encodeCTCP $ unwords+ [ "DCC SEND"+ , pathToBS name+ , ipToBigEndianBS ip+ , "0"+ , fileOffsetToBS size+ , tokenToBS t+ ] - > DCC ACCEPT <fileName> <port> <position>+ fromCtcp = parseCtcp $+ Send+ <$> ("DCC SEND" *> spaced path)+ <*> spaced ipBigEndian+ <*> spaced tcpPort+ <*> (Just <$> spaced fileOffset+ <|> return Nothing)+ <|> SendReverseServer+ <$> ("DCC SEND" *> spaced path)+ <*> spaced ipBigEndian+ <*> (spaced "0" *> spaced fileOffset)+ <*> spaced token - Reverse DCC:+-- | Signal intent to resume DCC file transfer at specific position+data DccResume+ {-| As part of the standard DCC protocol, sent by the client - > DCC ACCEPT <fileName> 0 <position> <token>+ > DCC RESUME <path> <port> <position> -}- = AcceptResumeFile TransferType FileMetadata FileOffset- deriving (Eq, Show)---- | Signal readiness to accept a connection (only Reverse DCC)-data OfferFileSink- {-| Reverse DCC:+ = Resume !Path !PortNumber !FileOffset+ {-| As part of the Reverse DCC protocol, sent by the client - > DCC SEND <fileName> <ip> <port> <fileSize> <token>+ > DCC RESUME <path> 0 <position> <token> -}- = OfferFileSink Token FileMetadata IPv4 PortNumber- deriving (Eq, Show)---- | Type of a DCC file transfer connection-data TransferType- -- | Connection where the owner of the file offers a socket to connect to- = Active IPv4 PortNumber- -- | Connection where the recipient of the file offers a socket to connect to- | Passive IPv4 Token- deriving (Eq, Show)---- | Properties of a file-data FileMetadata = FileMetadata { fileName :: Path Rel File- , fileSize :: Maybe FileOffset }- deriving (Eq, Show)---- | An identifier for knowing which negotiation a request belongs to-newtype Token = Token ByteString+ | ResumeReverse !Path !FileOffset !Token deriving (Eq, Show) -newtype FileOffset = FileOffset { toWord :: Word64 }- deriving (Eq, Ord, Num, Integral, Enum, Real, Bounded)--instance Show FileOffset where- show = show . toWord---- | Class for types that can be sent as CTCP commands-class CtcpCommand a where- encodeCtcp :: a -> CTCPByteString--instance CtcpCommand ByteString where- encodeCtcp = encodeCTCP--instance CtcpCommand OpenChat where- encodeCtcp = encodeCtcp . encodeOpenChat--instance CtcpCommand CloseChat where- encodeCtcp = encodeCtcp . encodeChatClose+instance CtcpCommand DccResume where+ toCtcp (Resume name port pos) = encodeCTCP $ unwords+ [ "DCC RESUME"+ , pathToBS name+ , tcpPortToBS port+ , fileOffsetToBS pos+ ]+ toCtcp (ResumeReverse name pos t) = encodeCTCP $ unwords+ [ "DCC RESUME"+ , pathToBS name+ , "0"+ , fileOffsetToBS pos+ , tokenToBS t+ ] -instance CtcpCommand OfferFile where- encodeCtcp = encodeCtcp . encodeOfferFile+ fromCtcp = parseCtcp $+ Resume+ <$> ("DCC RESUME" *> spaced path)+ <*> spaced tcpPort+ <*> spaced fileOffset+ <|> ResumeReverse+ <$> ("DCC RESUME" *> spaced path)+ <*> (spaced "0" *> spaced fileOffset)+ <*> spaced token -instance CtcpCommand TryResumeFile where- encodeCtcp = encodeCtcp . encodeTryResumeFile+-- | Signal acceptance to resume DCC file transfer at specific position+data DccAccept+ {-| As part of the standard DCC protocol, sent by the server -instance CtcpCommand AcceptResumeFile where- encodeCtcp = encodeCtcp . encodeAcceptResume+ > DCC ACCEPT <path> <port> <position>+ -}+ = Accept !Path !PortNumber !FileOffset+ {-| As part of the Reverse DCC protocol, sent by the server -instance CtcpCommand OfferFileSink where- encodeCtcp = encodeCtcp . encodeOfferFileSink+ > DCC ACCEPT <path> 0 <position> <token>+ -}+ | AcceptReverse !Path !FileOffset !Token+ deriving (Eq, Show) -runParser :: Parser a -> CTCPByteString -> Either String a-runParser p = parseOnly p . decodeCTCP+acceptedPosition :: DccAccept -> FileOffset+acceptedPosition (Accept _ _ pos) = pos+acceptedPosition (AcceptReverse _ pos _) = pos -parseService :: Parser Service-parseService = Messaging <$> parseOpenChat- <|> FileTransfer <$> parseOfferFile+instance CtcpCommand DccAccept where+ toCtcp (Accept name port pos) = encodeCTCP $ unwords+ [ "DCC ACCEPT"+ , pathToBS name+ , tcpPortToBS port+ , fileOffsetToBS pos+ ]+ toCtcp (AcceptReverse name pos t) = encodeCTCP $ unwords+ [ "DCC ACCEPT"+ , pathToBS name+ , "0"+ , fileOffsetToBS pos+ , tokenToBS t+ ] -encodeService :: Service -> ByteString-encodeService (Messaging m) = encodeOpenChat m-encodeService (FileTransfer o) = encodeOfferFile o+ fromCtcp = parseCtcp $+ Accept+ <$> ("DCC ACCEPT" *> spaced path)+ <*> spaced tcpPort+ <*> spaced fileOffset+ <|> AcceptReverse+ <$> ("DCC ACCEPT" *> spaced path)+ <*> (spaced "0" *> spaced fileOffset)+ <*> spaced token -parseOpenChat :: Parser OpenChat-parseOpenChat = choice [ do "DCC CHAT chat "- (i, p) <- parseSocket- endOfInput- return $ Chat i p- , do "DCC CHAT wboard "- (i, p) <- parseSocket- endOfInput- return $ Whiteboard i p- ]+-- | Tell the server to start a DCC file transfer and where it should send the data to.+data DccSendReverseClient+ {-| As part of the Reverse DCC protocol, sent by the client -encodeOpenChat :: OpenChat -> ByteString-encodeOpenChat (Chat i p) = "DCC CHAT chat " <> encodeSocket (i, p)-encodeOpenChat (Whiteboard i p) = "DCC CHAT wboard " <> encodeSocket (i, p)+ > DCC SEND <path> <ip> <port> <fileSize> <token>+ -}+ = SendReverseClient !Path !IPv4 !PortNumber !FileOffset !Token+ deriving (Eq, Show) -parseCloseChat :: Parser CloseChat-parseCloseChat = do "DCC CLOSE"- endOfInput- return CloseChat+instance CtcpCommand DccSendReverseClient where+ toCtcp (SendReverseClient name ip port size t) = encodeCTCP $ unwords+ [ "DCC SEND"+ , pathToBS name+ , ipToBigEndianBS ip+ , tcpPortToBS port+ , fileOffsetToBS size+ , tokenToBS t+ ] -encodeChatClose :: CloseChat -> ByteString-encodeChatClose _ = "DCC CLOSE"+ fromCtcp = parseCtcp $+ SendReverseClient+ <$> ("DCC SEND" *> spaced path)+ <*> spaced ipBigEndian+ <*> spaced tcpPort+ <*> spaced fileOffset+ <*> spaced token -parseOfferFile :: Parser OfferFile-parseOfferFile =- do "DCC SEND "- fn <- parseFileName- space- i <- parseIpBigEndian- space- choice [ do p <- parseTcpPort- fs <- Just <$> (space *> parseFileOffset)- <|> return Nothing- endOfInput- return (OfferFile- (Active i p)- (FileMetadata fn fs))- , do "0"- space- fs <- parseFileOffset- space- t <- parseToken- endOfInput- return (OfferFile- (Passive i t)- (FileMetadata fn (Just fs)))- ]+data PathType+ = Simple+ -- ^ A file path without spaces+ | Quoted+ -- ^ A file path that can include spaces and will be quoted when serialized+ deriving (Eq, Show) -encodeOfferFile :: OfferFile -> ByteString-encodeOfferFile (OfferFile (Active i p) (FileMetadata fn fs)) =- "DCC SEND "- <> encodeFileName fn- <> " " <> encodeIpBigEndian i- <> " " <> encodeTcpPort p- <> appendSpacedIfJust (encodeFileOffset <$> fs)-encodeOfferFile (OfferFile (Passive i t) (FileMetadata fn fs)) =- "DCC SEND "- <> encodeFileName fn- <> " " <> encodeIpBigEndian i- <> " 0"- <> appendSpacedIfJust (encodeFileOffset <$> fs)- <> " " <> encodeToken t+data Path = Rel PathType (P.Path P.Rel P.File)+ | Abs PathType (P.Path P.Abs P.File)+ deriving (Eq, Show) -parseTryResumeFile :: OfferFile -> Parser TryResumeFile-parseTryResumeFile (OfferFile tt f) =- do "DCC RESUME"- space- fn' <- parseFileName- when (fn' /= fileName f) $- fail "File name for resume didn't match file name in offer."- space- pos <- case tt of- Active _ p -> do- string (encodeTcpPort p)- space- parseFileOffset- Passive _ t -> do- "0"- space- pos <- parseFileOffset- space- string (encodeToken t)- return pos- endOfInput- return (TryResumeFile tt f pos)+fromPath :: Path -> P.Path P.Rel P.File+fromPath (Rel _ name) = name+fromPath (Abs _ name) = P.filename name +path :: Parser Path+path = choice [ quoted >>= parseRelOrAbs Quoted+ , simple >>= parseRelOrAbs Simple ]+ where+ quoted = UTF8.toString <$> ("\"" *> takeWhile1 (/= '"') <* "\"")+ simple = UTF8.toString <$> takeWhile1 (/= ' ')+ parseRelOrAbs ty n =+ maybe (fail "Could not parse file name.") return+ ( Rel ty <$> P.parseRelFile n+ <|> Abs ty <$> P.parseAbsFile n ) -encodeTryResumeFile :: TryResumeFile -> ByteString-encodeTryResumeFile (TryResumeFile (Active _ p) (FileMetadata fn _) pos) =- "DCC RESUME "- <> encodeFileName fn- <> " " <> encodeTcpPort p- <> " " <> encodeFileOffset pos-encodeTryResumeFile (TryResumeFile (Passive _ t) (FileMetadata fn _) pos) =- "DCC RESUME "- <> encodeFileName fn- <> " 0 "- <> encodeFileOffset pos- <> " " <> encodeToken t+pathToBS :: Path -> ByteString+pathToBS (Rel ty name) = wrap ty . UTF8.fromString . P.fromRelFile $ name+pathToBS (Abs ty name) = wrap ty . UTF8.fromString . P.fromAbsFile $ name -parseAcceptResumeFile :: TryResumeFile -> Parser AcceptResumeFile-parseAcceptResumeFile (TryResumeFile tt f _) =- do "DCC ACCEPT "- fn' <- parseFileName- when (fn' /= fileName f) $- fail "File name for accepting resume didn't match file name in offer."- space- ackPos <- case tt of- Active _ p -> do- string (encodeTcpPort p)- space- parseFileOffset- Passive _ t -> do- "0"- space- ackPos <- parseFileOffset- space- string (encodeToken t)- return ackPos- endOfInput- return (AcceptResumeFile tt f ackPos)+wrap :: PathType -> ByteString -> ByteString+wrap Simple p = p+wrap Quoted p = "\"" <> p <> "\"" -encodeAcceptResume :: AcceptResumeFile -> ByteString-encodeAcceptResume (AcceptResumeFile (Active _ p) (FileMetadata fn _) pos) =- "DCC ACCEPT "- <> encodeFileName fn- <> " " <> encodeTcpPort p- <> " " <> encodeFileOffset pos-encodeAcceptResume (AcceptResumeFile (Passive _ t) (FileMetadata fn _) pos) =- "DCC ACCEPT "- <> encodeFileName fn- <> " 0 "- <> encodeFileOffset pos- <> " " <> encodeToken t+newtype FileOffset = FileOffset { toWord :: Word64 }+ deriving (Eq, Ord, Num, Integral, Enum, Real, Bounded) -parseOfferFileSink :: AcceptResumeFile -> Parser (Maybe OfferFileSink)-parseOfferFileSink (AcceptResumeFile (Passive _ t) f _) =- do "DCC SEND "- fn <- parseFileName- when (fn /= fileName f) $- fail "File name for accepting resume didn't match file name in offer."- space- (i, p) <- parseSocket- string (appendSpacedIfJust (encodeFileOffset <$> fileSize f))- space- string (encodeToken t)- endOfInput- return (Just (OfferFileSink t f i p))-parseOfferFileSink _ = return Nothing+instance Show FileOffset where+ show = show . toWord -encodeOfferFileSink :: OfferFileSink -> ByteString-encodeOfferFileSink (OfferFileSink t (FileMetadata fn fs) i p) =- "DCC SEND "- <> encodeFileName fn- <> " " <> encodeSocket (i, p)- <> appendSpacedIfJust (encodeFileOffset <$> fs)- <> " " <> encodeToken t+fileOffset :: Parser FileOffset+fileOffset = FileOffset <$> decimal -parseSocket :: Parser (IPv4, PortNumber)-parseSocket =- do i <- parseIpBigEndian- space- p <- parseTcpPort- return (i, p)+fileOffsetToBS :: FileOffset -> ByteString+fileOffsetToBS = pack . show . toWord -encodeSocket :: (IPv4, PortNumber) -> ByteString-encodeSocket (i, p) = encodeIpBigEndian i <> " " <> encodeTcpPort p+-- | An identifier for knowing which negotiation a request belongs to+newtype Token = Token ByteString+ deriving (Eq, Show) -parseFileName :: Parser (Path Rel File)-parseFileName = do name <- "\"" *> takeWhile1 (/= '"') <* "\""- <|> takeWhile1 (/= ' ')- case parseRelOrAbs (UTF8.toString name) of- Nothing -> fail "Could not parse file name."- Just path -> return path- where parseRelOrAbs n = parseRelFile n- <|> filename <$> parseAbsFile n+token :: Parser Token+token = Token <$> takeByteString -encodeFileName :: Path Rel File -> ByteString-encodeFileName = pack . fromRelFile+tokenToBS :: Token -> ByteString+tokenToBS (Token t) = t -parseIpBigEndian :: Parser IPv4-parseIpBigEndian = fromBigEndianIp <$> parseBoundedInteger 0 4294967295+socket :: Parser (IPv4, PortNumber)+socket = (,) <$> ipBigEndian <* space <*> tcpPort -encodeIpBigEndian :: IPv4 -> ByteString-encodeIpBigEndian = pack . show . toBigEndianIp+socketToBS :: (IPv4, PortNumber) -> ByteString+socketToBS (i, p) = ipToBigEndianBS i <> " " <> tcpPortToBS p -parseTcpPort :: Parser PortNumber-parseTcpPort = fromInteger <$> parseBoundedInteger 1 65535+ipBigEndian :: Parser IPv4+ipBigEndian = fromBigEndianIp <$> decimalInRange (0, 4294967295) -encodeTcpPort :: PortNumber -> ByteString-encodeTcpPort = pack . show+ipToBigEndianBS :: IPv4 -> ByteString+ipToBigEndianBS = pack . show . toBigEndianIp -parseFileOffset :: Parser FileOffset-parseFileOffset = FileOffset <$> decimal+fromBigEndianIp :: Integer -> IPv4+fromBigEndianIp = fromHostAddress . byteSwap32 . fromIntegral -encodeFileOffset :: FileOffset -> ByteString-encodeFileOffset = pack . show . toWord+toBigEndianIp :: IPv4 -> Integer+toBigEndianIp = fromIntegral . byteSwap32 . toHostAddress -parseToken :: Parser Token-parseToken = Token <$> takeByteString+tcpPort :: Parser PortNumber+tcpPort = fromInteger <$> decimalInRange (1, 65535) -encodeToken :: Token -> ByteString-encodeToken (Token t) = t+tcpPortToBS :: PortNumber -> ByteString+tcpPortToBS = pack . show -parseBoundedInteger :: Integer -> Integer -> Parser Integer-parseBoundedInteger lower upper = do+decimalInRange :: (Integer, Integer) -> Parser Integer+decimalInRange (lower, upper) = do num <- decimal when (num < lower || num > upper) $- fail ("Failed to parse " ++ show num ++ ", not in range [" ++- show lower ++ ", " ++ show upper ++ "].")+ fail ( "Failed to parse " ++ show num ++ ", not in range ["+ ++ show lower ++ ", " ++ show upper ++ "]." ) return num -appendSpacedIfJust :: Maybe ByteString -> ByteString-appendSpacedIfJust (Just x) = " " <> x-appendSpacedIfJust Nothing = mempty--fromBigEndianIp :: Integer -> IPv4-fromBigEndianIp = fromHostAddress . byteSwap32 . fromIntegral--toBigEndianIp :: IPv4 -> Integer-toBigEndianIp = fromIntegral . byteSwap32 . toHostAddress+spaced :: Parser a -> Parser a+spaced = (space *>)
src/Network/Socket/ByteString/Extended.hs view
@@ -1,77 +1,85 @@ -- | Common functions simplyfing the use of "Network.Socket.ByteString" module Network.Socket.ByteString.Extended- ( module Network.Socket- , module Network.Socket.ByteString- , Sink(..)- , Source(..)- , withActiveSocket- , withPassiveSocket+ ( ConnectionType(..)+ , Socket(..)+ , S.PortNumber+ , connect+ , close+ , recv+ , sendAll , toNetworkByteOrder ) where -import Control.Error-import Control.Monad.IO.Class (liftIO)+import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Binary.Put (putWord32be, runPut) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Lazy.Char8 as Lazy (toStrict)-import Data.IP (IPv4, fromHostAddress,- toHostAddress)-import Network.Socket hiding (recv, recvFrom, send,- sendTo)-import Network.Socket.ByteString-import System.Timeout+import Data.IP (IPv4, toHostAddress)+import Data.Maybe (fromMaybe)+import qualified Network.Socket as S hiding (recv, recvFrom, send,+ sendTo)+import qualified Network.Socket.ByteString as S -data Sink = Sink IPv4 PortNumber+data ConnectionType m+ = Active !IPv4 !S.PortNumber (m ())+ -- ^ Connects to other party on specified port. With callback when socket is ready.+ | Passive !IPv4 !(Maybe S.PortNumber) (S.PortNumber -> m ())+ -- ^ Binds to local port and waits for connection by other party. If no port number+ -- is provided, one will be provided by the OS. With callback when socket is ready. -data Source = Source IPv4 (Maybe PortNumber)+data Socket+ = ActiveSocket !S.Socket+ | PassiveSocket !S.Socket !S.PortNumber --- | Run functions on socket when connected and close socket afterwards-withActiveSocket :: Sink- -> (PortNumber -> ExceptT String IO ())- -- ^ Callback when socket is ready- -> (Socket -> IO ())- -- ^ Callback when socket is connected to server- -> ExceptT String IO ()-withActiveSocket (Sink i p) onListen onConnected = do- liftIO $ return withSocketsDo- sock <- liftIO $ socket AF_INET Stream defaultProtocol- onListen p- liftIO $ connect sock (SockAddrInet p (toHostAddress i))- liftIO $ onConnected sock- liftIO $ sClose sock+socket :: Socket -> S.Socket+socket (ActiveSocket sock) = sock+socket (PassiveSocket sock _) = sock -{- | Run functions on passive socket when listening and when connected and close- socket afterwards.--}-withPassiveSocket :: Source- -> (PortNumber -> ExceptT String IO ())- -- ^ Callback when socket is open and listening- -> (Socket -> IO ())- -- ^ Callback when client connected to socket- -> ExceptT String IO ()-withPassiveSocket (Source i maybeP) onListen onConnected = do- liftIO $ return withSocketsDo- sock <- liftIO $ openListenSocket (fromMaybe aNY_PORT maybeP)- p <- liftIO $ socketPort sock- onListen p- accepted <- liftIO $ timeout 10000000 $ accept sock+connect :: MonadIO m => ConnectionType m -> m Socket+connect (Active ip port onListen) = do+ sock <- liftIO $ connectTo ip port+ onListen+ liftIO $ waitForConnection ip sock+connect (Passive ip maybePort onListen) = do+ sock@(PassiveSocket _ port) <- liftIO $ listenOn maybePort+ onListen port+ liftIO $ waitForConnection ip sock++close :: Socket -> IO ()+close = S.close . socket++recv :: Socket -> Int -> IO ByteString+recv = S.recv . socket++sendAll :: Socket -> ByteString -> IO ()+sendAll = S.sendAll . socket++connectTo :: IPv4 -> S.PortNumber -> IO Socket+connectTo ip port = S.withSocketsDo $ do+ sock <- S.socket S.AF_INET S.Stream S.defaultProtocol+ S.connect sock (S.SockAddrInet port (toHostAddress ip))+ return $ ActiveSocket sock++listenOn :: Maybe S.PortNumber -> IO Socket+listenOn port = S.withSocketsDo $ do+ sock <- openListenSocket (fromMaybe S.aNY_PORT port)+ port' <- S.socketPort sock+ return $ PassiveSocket sock port'++waitForConnection :: IPv4 -> Socket -> IO Socket+waitForConnection _ sock@(ActiveSocket _) = return sock -- Already connected+waitForConnection ip (PassiveSocket sock port) = do+ accepted <- S.accept sock case accepted of- Just (con, SockAddrInet _ client)- | client == toHostAddress i -> liftIO $ do onConnected con- sClose con- | otherwise -> do liftIO $ sClose con- throwE ( "Expected connection from host "- ++ show (fromHostAddress client)- ++ ", not from " ++ show i ++". Aborting…\n" )- _ -> throwE ( "Timeout when waiting for other party to connect on port "- ++ show p ++ "…\n")- liftIO $ sClose sock+ (con, S.SockAddrInet _ client)+ | client == toHostAddress ip -> return $ PassiveSocket con port+ _ -> fail ( "Connection did not come from " ++ show ip ++ " as expected." ) -openListenSocket :: PortNumber -> IO Socket+openListenSocket :: S.PortNumber -> IO S.Socket openListenSocket p = do- sock <- socket AF_INET Stream defaultProtocol- bind sock (SockAddrInet p iNADDR_ANY)- listen sock 1+ sock <- S.socket S.AF_INET S.Stream S.defaultProtocol+ S.bind sock (S.SockAddrInet p S.iNADDR_ANY)+ S.listen sock 1 return sock -- | Converts numbers to a '32bit unsigned int' in network byte order.
+ src/System/IO/Streams/Lifted.hs view
@@ -0,0 +1,32 @@+-- | Some functions generalized from "System.IO.Streams"+module System.IO.Streams.Lifted+ ( withBinaryFile+ , withFileAsOutputExt+ ) where++import Control.Exception.Safe (MonadMask, bracket)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.ByteString.Char8 (ByteString)+import System.IO (BufferMode, Handle, IOMode, hClose,+ hSetBuffering, openBinaryFile)+import System.IO.Streams (OutputStream, handleToOutputStream)++withBinaryFile :: (MonadMask m, MonadIO m)+ => FilePath+ -> IOMode+ -> (Handle -> m a)+ -> m a+withBinaryFile name mode =+ bracket (liftIO $ openBinaryFile name mode)+ (liftIO . hClose)++withFileAsOutputExt :: (MonadMask m, MonadIO m)+ => FilePath+ -> IOMode+ -> BufferMode+ -> (OutputStream ByteString -> m a)+ -> m a+withFileAsOutputExt name mode buffermode f =+ withBinaryFile name mode $ \h -> do+ liftIO $ hSetBuffering h buffermode+ liftIO (handleToOutputStream h) >>= f
+ test/Network/IRC/DCCTest.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE TemplateHaskell #-}++module Network.IRC.DCCTest where++import Network.IRC.DCC.Internal++import Control.Monad (replicateM)+import Data.ByteString.Char8 (pack)+import Data.IP (IPv4, toIPv4)+import Network.Socket (PortNumber)+import Path (mkAbsFile, mkRelFile)+import Test.Hspec.Attoparsec+import Test.Tasty (TestTree)+import Test.Tasty.Hspec (describe, it, testSpec)+import Test.Tasty.QuickCheck (Arbitrary (..), Gen, choose,+ elements, forAll, oneof, property)++instance Arbitrary Token where+ arbitrary = Token . pack <$> arbitrary++instance Arbitrary PortNumber where+ arbitrary = fromIntegral <$> (choose (1, 65535) :: Gen Int)++instance Arbitrary IPv4 where+ arbitrary = toIPv4 <$> replicateM 4 (choose (0, 255))++instance Arbitrary Path where+ arbitrary = elements [ Rel Simple $(mkRelFile "filename")+ , Rel Simple $(mkRelFile "filename.txt")+ , Rel Simple $(mkRelFile "file\8195name.txt")+ , Rel Simple $(mkRelFile "\128110\127997")+ , Abs Simple $(mkAbsFile "/home/user/dirname/filename.txt")+ , Rel Quoted $(mkRelFile "filename.txt")+ , Rel Quoted $(mkRelFile "file name.txt")+ , Rel Quoted $(mkRelFile "file\8195 name.txt")+ , Rel Quoted $(mkRelFile "\128110\127997")+ , Abs Quoted $(mkAbsFile "/home/user/dir name/filename.txt")+ ]++instance Arbitrary FileOffset where+ arbitrary = FileOffset <$> arbitrary++instance Arbitrary DccChat where+ arbitrary = oneof [ Chat <$> arbitrary <*> arbitrary+ , ChatWhiteboard <$> arbitrary <*> arbitrary+ ]++instance Arbitrary DccSend where+ arbitrary = oneof [ Send <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary+ , SendReverseServer <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary+ ]++instance Arbitrary DccResume where+ arbitrary = oneof [ Resume <$> arbitrary <*> arbitrary <*> arbitrary+ , ResumeReverse <$> arbitrary <*> arbitrary <*> arbitrary+ ]++instance Arbitrary DccAccept where+ arbitrary = oneof [ Accept <$> arbitrary <*> arbitrary <*> arbitrary+ , AcceptReverse <$> arbitrary <*> arbitrary <*> arbitrary+ ]++instance Arbitrary DccSendReverseClient where+ arbitrary = SendReverseClient+ <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary++spec :: IO TestTree+spec = testSpec "DCC message serialization" $ do++ describe "Parsing single elements" $ do++ describe "Network byte order IP" $ do+ describe "[SUCCESS]" $ do+ it "parse . encode == id" $ property $ \ip ->+ ipToBigEndianBS ip ~> ipBigEndian `parseSatisfies` (== ip)+ it "At beginning of stream" $+ pack "0abcd" ~?> ipBigEndian+ `leavesUnconsumed` pack "abcd"+ describe "[FAILURE]" $ do+ it "Parse < 0.0.0.0" $+ forAll (choose (minBound, -1 :: Int)) $ \ip ->+ ipBigEndian `shouldFailOn` pack (show ip)+ it "Parse > 255.255.255.255" $+ forAll (choose (4294967296, maxBound :: Int)) $ \ip ->+ ipBigEndian `shouldFailOn` pack (show ip)+ it "Non-digits" $+ ipBigEndian `shouldFailOn` pack "abcd"+ it "When not at beginning of stream" $+ ipBigEndian `shouldFailOn` pack " 0"++ describe "File name" $ do+ describe "[SUCCESS]" $ do+ it "parse . encode == id" $ property $ \name ->+ pathToBS name ~> path `parseSatisfies` (== name)+ it "At beginning of stream" $+ pack "filename 122350" ~?> path+ `leavesUnconsumed` pack " 122350"+-- TODO Not sure exactly what to do with this yet. On Unix the whole thing+-- could be a file name, while on windows it is obviously an absolute path+-- it "File name from absolute unix path" $+-- ("c:\\Users\\user\\filename.txt" ~> path+-- `shouldParse` $(mkRelFile "filename.txt")+ describe "[FAILURE]" $ do+ it "ASCII filename with space" $+ pack "file name.txt" ~?> path+ `leavesUnconsumed` pack " name.txt"+ it "Not at beginning of stream" $+ path `shouldFailOn` pack " filename"++ describe "Token" $+ describe "[SUCCESS]" $+ it "parse . encode == id" $ property $ \t ->+ tokenToBS t ~> token `parseSatisfies` (== t)++ describe "TCP port" $ do+ describe "[SUCCESS]" $+ it "parse . encode == id" $ property $ \p ->+ tcpPortToBS p ~> tcpPort `parseSatisfies` (== p)+ describe "[FAILURE]" $ do+ it "Parse <= 0" $+ forAll (choose (minBound, 0 :: Int)) $ \p ->+ tcpPort `shouldFailOn` pack (show p)+ it "Parse > 65535" $+ forAll (choose (65535, maxBound :: Int)) $ \p ->+ tcpPort `shouldFailOn` pack (show p)++ describe "File offset" $ do+ describe "[SUCCESS]" $+ it "parse . encode == id" $ property $ \o ->+ fileOffsetToBS o ~> fileOffset `parseSatisfies` (== o)+ describe "[FAILURE]" $+ it "Parse < 0" $+ forAll (choose (minBound, -1 :: Int)) $ \o ->+ fileOffset `shouldFailOn` pack (show o)++ describe "DCC commands" $ do++ describe "DCC CHAT" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $ property $ \cmd ->+ fromCtcp (toCtcp cmd) == Right (cmd :: DccChat)++ describe "DCC CLOSE" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $+ fromCtcp (toCtcp Close) == Right Close++ describe "DCC SEND" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $ property $ \cmd ->+ fromCtcp (toCtcp cmd) == Right (cmd :: DccSend)++ describe "DCC RESUME" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $ property $ \cmd ->+ fromCtcp (toCtcp cmd) == Right (cmd :: DccResume)++ describe "DCC ACCEPT" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $ property $ \cmd ->+ fromCtcp (toCtcp cmd) == Right (cmd :: DccAccept)++ describe "DCC SEND (reverse client)" $+ describe "[SUCCESS]" $+ it "fromCtcp . toCtcp == id" $ property $ \cmd ->+ fromCtcp (toCtcp cmd) == Right (cmd :: DccSendReverseClient)
+ test/Spec.hs view
@@ -0,0 +1,5 @@+import qualified Network.IRC.DCCTest as DCC+import Test.Tasty++main :: IO ()+main = DCC.spec >>= defaultMain
− tests/Main.hs
@@ -1,7 +0,0 @@-module Main where--import qualified Network.IRC.DCCTest as DCC-import Test.Tasty--main :: IO ()-main = DCC.spec >>= defaultMain
− tests/Network/IRC/DCCTest.hs
@@ -1,118 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Network.IRC.DCCTest where--import Network.IRC.DCC.Internal--import Data.ByteString.Char8 (pack)-import qualified Data.ByteString.UTF8 as UTF8 (fromString)-import Data.IP (toIPv4)-import Path (mkRelFile)-import Test.Hspec.Attoparsec-import Test.Tasty (TestTree)-import Test.Tasty.Hspec (describe, it, testSpec)--spec :: IO TestTree-spec = testSpec "DCC message serialization" $-- describe "Parsing single elements" $ do-- describe "Network byte order IP" $ do-- describe "[SUCCESS]" $ do-- it "Min IPv4" $- pack "0" ~> parseIpBigEndian- `shouldParse` toIPv4 [0, 0, 0, 0]-- it "Max IPv4" $- pack "4294967295" ~> parseIpBigEndian- `shouldParse` toIPv4 [255, 255, 255, 255]-- it "Local IPv4" $- pack "3232235521" ~> parseIpBigEndian- `shouldParse` toIPv4 [192, 168, 0, 1]-- it "Public IPv4" $- pack "134743044" ~> parseIpBigEndian- `shouldParse` toIPv4 [8, 8, 4, 4]-- it "IPv4 at beginning of stream" $- pack "0abcd" ~?> parseIpBigEndian- `leavesUnconsumed` pack "abcd"-- describe "[FAILURE]" $ do-- it "Negative IPv4" $- parseIpBigEndian `shouldFailOn` pack "-1"-- it "Bigger than max IPv4" $- parseIpBigEndian `shouldFailOn` pack "4294967296"-- it "Max IPv4 with additional digit" $- parseIpBigEndian `shouldFailOn` pack "42949672950"-- it "Non-digits" $- parseIpBigEndian `shouldFailOn` pack "abcd"-- it "When not at beginning of stream" $- parseIpBigEndian `shouldFailOn` pack " 0"-- describe "File name" $ do-- describe "[SUCCESS]" $ do-- it "Without extension" $- pack "filename" ~> parseFileName- `shouldParse` $(mkRelFile "filename")-- it "With extension" $- pack "filename.txt" ~> parseFileName- `shouldParse` $(mkRelFile "filename.txt")-- it "Quoted with extension" $- pack "\"filename.txt\"" ~> parseFileName- `shouldParse` $(mkRelFile "filename.txt")-- it "Quoted with space" $- pack "\"file name.txt\"" ~> parseFileName- `shouldParse` $(mkRelFile "file name.txt")-- it "UTF8 with em space" $- UTF8.fromString "file\8195name.txt" ~> parseFileName- `shouldParse` $(mkRelFile "file\8195name.txt")-- it "UTF8 with skin tone modifier" $- UTF8.fromString "\128110\127997" ~> parseFileName- `shouldParse` $(mkRelFile "\128110\127997")-- it "Quoted UTF8 with space" $- UTF8.fromString "\"file\8195 name.txt\"" ~> parseFileName- `shouldParse` $(mkRelFile "file\8195 name.txt")-- it "From absolute unix path" $- pack "/home/user/filename.txt" ~> parseFileName- `shouldParse` $(mkRelFile "filename.txt")-- it "From quoted absolute unix path" $- pack "\"/home/user/filename.txt\"" ~> parseFileName- `shouldParse` $(mkRelFile "filename.txt")-- it "At beginning of stream" $- pack "filename 122350" ~?> parseFileName- `leavesUnconsumed` pack " 122350"---- TODO Not sure exactly what to do with this yet. On Unix the whole thing--- could be a file name, while on windows it is obviously an absolute path--- it "File name from absoulte unix path" $--- ("c:\\Users\\user\\filename.txt" ~> parseFileName--- `shouldParse` $(mkRelFile "filename.txt")-- describe "[FAILURE]" $ do-- it "ASCII filename with space" $- pack "file name.txt" ~?> parseFileName- `leavesUnconsumed` pack " name.txt"-- it "Not at beginning of stream" $- parseFileName `shouldFailOn` pack " filename"