packages feed

hookup 0.2 → 0.2.2

raw patch · 4 files changed

+621/−62 lines, 4 filesdep +attoparsecdep −socksdep ~basedep ~networkPVP ok

version bump matches the API change (PVP)

Dependencies added: attoparsec

Dependencies removed: socks

Dependency ranges changed: base, network

API changes (from Hackage documentation)

+ Hookup: CommandReply :: Word8 -> CommandReply
+ Hookup: SocksAuthenticationError :: ConnectionFailure
+ Hookup: SocksBadDomainName :: ConnectionFailure
+ Hookup: SocksError :: CommandReply -> ConnectionFailure
+ Hookup: SocksProtocolError :: ConnectionFailure
+ Hookup: connectWithSocket :: ConnectionParams -> Socket -> IO Connection
+ Hookup: defaultTlsParams :: TlsParams
+ Hookup: newtype CommandReply
+ Hookup: putBuf :: Connection -> ByteString -> IO ()
+ Hookup: recv :: Connection -> Int -> IO ByteString

Files

ChangeLog.md view
@@ -1,5 +1,9 @@ # Revision history for hookup +## 0.2.1 -- 2018-07++* Added `connectWithSocket`, `recv`, `putBuf`, `defaultTlsParams`+ ## 0.2 -- 2017-11-22  * Allow connection parameters to specify address family with `cpFamily` field
hookup.cabal view
@@ -1,5 +1,5 @@ name:                hookup-version:             0.2+version:             0.2.2 synopsis:            Abstraction over creating network connections with SOCKS5 and TLS description:         This package provides an abstraction for communicating with line-oriented                      network services while abstracting over the use of SOCKS5 and TLS (via OpenSSL)@@ -23,12 +23,13 @@  library   exposed-modules:     Hookup-  other-modules:       Hookup.OpenSSL+  other-modules:       Hookup.OpenSSL,+                       Hookup.Socks5   extra-libraries:     ssl-  build-depends:       base                  >=4.9  && <4.11,-                       socks                 >=0.5  && <0.6,-                       network               >=2.6  && <2.7,+  build-depends:       base                  >=4.9  && <4.12,+                       network               >=2.6  && <2.8,                        bytestring            >=0.10 && <0.11,+                       attoparsec            >=0.13 && <0.14,                        HsOpenSSL             >=0.11.2.3 && <0.12,                        HsOpenSSL-x509-system >=0.1  && <0.2   hs-source-dirs:      src
src/Hookup.hs view
@@ -7,24 +7,47 @@  This module provides a uniform interface to network connections with optional support for TLS and SOCKS.++This library is careful to support both IPv4 and IPv6. It will attempt to+all of the addresses that a domain name resolves to until one the first+successful connection.++Use 'connect' and 'close' to establish and close network connections.++Use 'recv', 'recvLine', and 'send' to receive and transmit data on an+open network connection.++TLS and SOCKS parameters can be provided. When both are provided a connection+will first be established to the SOCKS server and then the TLS connection will+be established through that proxy server. This is most useful when connecting+through a dynamic port forward of an SSH client via the @-D@ flag.+ -} module Hookup   (+  -- * Connections+  Connection,+  connect,+  connectWithSocket,+  close,++  -- * Reading and writing data+  recv,+  recvLine,+  send,+  putBuf,+   -- * Configuration   ConnectionParams(..),   SocksParams(..),   TlsParams(..),   defaultFamily,+  defaultTlsParams, -  -- * Connections-  Connection,-  connect,-  recvLine,-  send,-  close,    -- * Errors   ConnectionFailure(..),+  CommandReply(..)   ) where  import           Control.Concurrent@@ -32,34 +55,49 @@ import           Control.Monad import           Data.ByteString (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as B8 import           Data.Foldable import           Data.List (intercalate)-import           Network (PortID(..)) import           Network.Socket (Socket, AddrInfo, PortNumber, HostName, Family) import qualified Network.Socket as Socket import qualified Network.Socket.ByteString as SocketB-import           Network.Socks5 import           OpenSSL.Session (SSL, SSLContext) import qualified OpenSSL as SSL import qualified OpenSSL.Session as SSL import qualified OpenSSL.X509 as SSL import           OpenSSL.X509.SystemStore import qualified OpenSSL.PEM as PEM+import           Data.Attoparsec.ByteString (Parser)+import qualified Data.Attoparsec.ByteString as Parser  import           Hookup.OpenSSL (installVerification)+import           Hookup.Socks5   -- | Parameters for 'connect'.+--+-- Common defaults for fields: 'defaultFamily', 'defaultTlsParams'+--+-- The address family can be specified in order to force only+-- IPv4 or IPv6 to be used. The default behavior is to support both.+-- It can be useful to specify exactly one of these in the case that+-- the other is misconfigured and a hostname is resolving to both.+--+-- When a 'SocksParams' is provided the connection will be established+-- using a SOCKS (version 5) proxy.+--+-- When a 'TlsParams' is provided the connection negotiate TLS at connect+-- time in order to protect the stream. data ConnectionParams = ConnectionParams   { cpFamily :: Family           -- ^ IP Protocol family (default 'AF_UNSPEC')   , cpHost  :: HostName          -- ^ Destination host   , cpPort  :: PortNumber        -- ^ Destination TCP port-  , cpSocks :: Maybe SocksParams -- ^ Optional SOCKS5 parameters+  , cpSocks :: Maybe SocksParams -- ^ Optional SOCKS parameters   , cpTls   :: Maybe TlsParams   -- ^ Optional TLS parameters   }  --- | SOCKS5 connection parameters+-- | SOCKS connection parameters data SocksParams = SocksParams   { spHost :: HostName   -- ^ SOCKS server host   , spPort :: PortNumber -- ^ SOCKS server port@@ -72,11 +110,10 @@   { tpClientCertificate  :: Maybe FilePath -- ^ Path to client certificate   , tpClientPrivateKey   :: Maybe FilePath -- ^ Path to client private key   , tpServerCertificate  :: Maybe FilePath -- ^ Path to CA certificate bundle-  , tpCipherSuite        :: String -- ^ OpenSSL cipher suite name (e.g. "HIGH")-  , tpInsecure           :: Bool -- ^ Disables certificate checking+  , tpCipherSuite        :: String -- ^ OpenSSL cipher suite name (e.g. @\"HIGH\"@)+  , tpInsecure           :: Bool -- ^ Disables certificate checking when 'True'   } - -- | Type for errors that can be thrown by this package. data ConnectionFailure   -- | Failure during 'getAddrInfo' resolving remote host@@ -87,6 +124,14 @@   | LineTooLong   -- | Incomplete line during 'recvLine'   | LineTruncated+  -- | Socks command rejected by server by given reply code+  | SocksError CommandReply+  -- | Socks authentication method was not accepted+  | SocksAuthenticationError+  -- | Socks server sent an invalid message or no message.+  | SocksProtocolError+  -- | Domain name was too long for SOCKS protocol+  | SocksBadDomainName   deriving Show  -- | 'displayException' implemented for prettier messages@@ -94,15 +139,45 @@   displayException LineTruncated = "connection closed while reading line"   displayException LineTooLong   = "line length exceeded maximum"   displayException (ConnectionFailure xs) =-    "connection attempts failed due to: " +++    "connection attempt failed due to: " ++       intercalate ", " (map displayException xs)   displayException (HostnameResolutionFailure x) =     "hostname resolution failed: " ++ displayException x+  displayException SocksAuthenticationError =+    "SOCKS authentication method rejected"+  displayException SocksProtocolError =+    "SOCKS server protocol error"+  displayException SocksBadDomainName =+    "SOCKS domain name length limit exceeded"+  displayException (SocksError reply) =+    "SOCKS command rejected: " +++    case reply of+      Succeeded         -> "succeeded"+      GeneralFailure    -> "general SOCKS server failure"+      NotAllowed        -> "connection not allowed by ruleset"+      NetUnreachable    -> "network unreachable"+      HostUnreachable   -> "host unreachable"+      ConnectionRefused -> "connection refused"+      TTLExpired        -> "TTL expired"+      CmdNotSupported   -> "command not supported"+      AddrNotSupported  -> "address type not supported"+      CommandReply n    -> "unknown reply " ++ show n  -- | Default 'Family' value is unspecified and allows both INET and INET6. defaultFamily :: Socket.Family defaultFamily = Socket.AF_UNSPEC +-- | Default values for TLS that use no client certificates, use+-- system CA root, @\"HIGH\"@ cipher suite, and which validate hostnames.+defaultTlsParams :: TlsParams+defaultTlsParams = TlsParams+  { tpClientCertificate  = Nothing+  , tpClientPrivateKey   = Nothing+  , tpServerCertificate  = Nothing -- use system provided CAs+  , tpCipherSuite        = "HIGH"+  , tpInsecure           = False+  }+ ------------------------------------------------------------------------ -- Opening sockets ------------------------------------------------------------------------@@ -113,16 +188,58 @@ openSocket params =   case cpSocks params of     Nothing -> openSocket' (cpFamily params) (cpHost params) (cpPort params)-    Just sp -> openSocks sp (cpHost params) (cpPort params)+    Just sp ->+      do sock <- openSocket' (cpFamily params) (spHost sp) (spPort sp)+         (sock <$ socksConnect sock (cpHost params) (cpPort params))+           `onException` Socket.close sock  -openSocks :: SocksParams -> HostName -> PortNumber -> IO Socket-openSocks sp h p =-  do socksConnectTo'-       (spHost sp) (PortNumber (spPort sp))-       h           (PortNumber p)+netParse :: Show a => Socket -> Parser a -> IO a+netParse sock parser =+  do -- receiving 1 byte at a time is not efficient, but these messages+     -- are very short and we don't want to read any more from the socket+     -- than is necessary+     result <- Parser.parseWith+                 (SocketB.recv sock 1)+                 parser+                 B.empty+     case result of+       Parser.Done i x | B.null i -> return x+       _ -> throwIO SocksProtocolError  +socksConnect :: Socket -> HostName -> PortNumber -> IO ()+socksConnect sock host port =+  do SocketB.sendAll sock $+       buildClientHello ClientHello+         { cHelloMethods = [AuthNoAuthenticationRequired] }++     validateHello =<< netParse sock parseServerHello++     let dnBytes = B8.pack host+     unless (B.length dnBytes < 256)+       (throwIO SocksBadDomainName)++     SocketB.sendAll sock $+       buildRequest Request+         { reqCommand  = Connect+         , reqAddress  = Address (DomainName dnBytes) port+         }++     validateResponse =<< netParse sock parseResponse+++validateHello :: ServerHello -> IO ()+validateHello hello =+  unless (sHelloMethod hello == AuthNoAuthenticationRequired)+    (throwIO SocksAuthenticationError)++validateResponse :: Response -> IO ()+validateResponse response =+  unless (rspReply response == Succeeded )+    (throwIO (SocksError (rspReply response)))++ openSocket' :: Family -> HostName -> PortNumber -> IO Socket openSocket' family h p =   do let hints = Socket.defaultHints@@ -137,16 +254,28 @@        Left  ioe -> throwIO (HostnameResolutionFailure ioe)  -attemptConnections :: [IOError] -> [Socket.AddrInfo] -> IO Socket+-- | Try establishing a connection to the services indicated by+-- a given list of 'AddrInfo' values. Either return a socket that+-- has successfully connected to one of the candidate 'AddrInfo's+-- or throw a 'ConnectionFailure' exception will all of the+-- encountered errors.+attemptConnections ::+  [IOError]         {- ^ accumulated errors  -} ->+  [Socket.AddrInfo] {- ^ candidate AddrInfos -} ->+  IO Socket         {- ^ connected socket    -} attemptConnections exs [] = throwIO (ConnectionFailure exs) attemptConnections exs (ai:ais) =-  do s <- socket' ai-     res <- try (Socket.connect s (Socket.addrAddress ai))+  do res <- try (connectToAddrInfo ai)      case res of-       Left ex -> do Socket.close s-                     attemptConnections (ex:exs) ais-       Right{} -> return s+       Left ex -> attemptConnections (ex:exs) ais+       Right s -> return s +-- | Create a socket and connect to the service identified+-- by the given 'AddrInfo' and return the connected socket.+connectToAddrInfo :: AddrInfo -> IO Socket+connectToAddrInfo info+  = bracketOnError (socket' info) Socket.close+  $ \s -> s <$ Socket.connect s (Socket.addrAddress info)  -- | Open a 'Socket' using the parameters from an 'AddrInfo' socket' :: AddrInfo -> IO Socket@@ -164,12 +293,14 @@ data NetworkHandle = SSL SSL | Socket Socket  -openNetworkHandle :: ConnectionParams -> IO NetworkHandle-openNetworkHandle params =-  do s <- openSocket params-     case cpTls params of-       Nothing -> return (Socket s)-       Just tp -> SSL <$> startTls (cpHost params) tp s+openNetworkHandle ::+  ConnectionParams {- ^ parameters             -} ->+  IO Socket        {- ^ socket creation action -} ->+  IO NetworkHandle {- ^ open network handle    -}+openNetworkHandle params mkSocket =+  case cpTls params of+    Nothing  -> Socket <$> mkSocket+    Just tls -> SSL <$> startTls tls (cpHost params) mkSocket   closeNetworkHandle :: NetworkHandle -> IO ()@@ -199,18 +330,53 @@ -- | Open network connection to TCP service specified by -- the given parameters. --+-- The resulting connection MUST be closed with 'close' to avoid leaking+-- resources.+-- -- Throws 'IOError', 'SocksError', 'SSL.ProtocolError', 'ConnectionFailure'-connect :: ConnectionParams -> IO Connection+connect ::+  ConnectionParams {- ^ parameters      -} ->+  IO Connection    {- ^ open connection -} connect params =-  do h <- openNetworkHandle params+  do h <- openNetworkHandle params (openSocket params)      b <- newMVar B.empty      return (Connection b h) +-- | Create a new 'Connection' using an already connected socket.+-- This will attempt to start TLS if configured but will ignore+-- any SOCKS server settings as it is assumed that the socket+-- is already actively connected to the intended service.+--+-- Throws 'SSL.ProtocolError'+connectWithSocket ::+  ConnectionParams {- ^ parameters       -} ->+  Socket           {- ^ connected socket -} ->+  IO Connection    {- ^ open connection  -}+connectWithSocket params sock =+  do h <- openNetworkHandle params (return sock)+     b <- newMVar B.empty+     return (Connection b h)  -- | Close network connection.-close :: Connection -> IO ()+close ::+  Connection {- ^ open connection -} ->+  IO () close (Connection _ h) = closeNetworkHandle h +-- | Receive the next chunk from the stream. This operation will first+-- return the buffer if it contains a non-empty chunk. Otherwise it will+-- request up to the requested number of bytes from the stream.+--+-- Throws: 'IOError', 'SSL.ConnectionAbruptlyTerminated', 'SSL.ProtocolError'+recv ::+  Connection    {- ^ open connection              -} ->+  Int           {- ^ maximum underlying recv size -} ->+  IO ByteString {- ^ next chunk from stream       -}+recv (Connection buf h) n =+  do bufChunk <- swapMVar buf B.empty+     if B.null bufChunk+       then networkRecv h n+       else return bufChunk  -- | Receive a line from the network connection. Both -- @"\\r\\n"@ and @"\\n"@ are recognized.@@ -222,38 +388,60 @@ -- can happen if the peer transmits some data and closes its end -- without transmitting a line terminator. ----- Throws: 'ConnectionAbruptlyTerminated', 'ConnectionFailure', 'IOError'-recvLine :: Connection -> Int -> IO (Maybe ByteString)+-- Throws: 'SSL.ConnectionAbruptlyTerminated', 'SSL.ProtocolError', 'ConnectionFailure', 'IOError'+recvLine ::+  Connection            {- ^ open connection            -} ->+  Int                   {- ^ maximum line length        -} ->+  IO (Maybe ByteString) {- ^ next line or end-of-stream -} recvLine (Connection buf h) n =   modifyMVar buf $ \bs ->     go (B.length bs) bs []   where+    -- bsn: cached length of concatenation of (bs:bss)+    -- bs : most recent chunk+    -- bss: other chunks ordered from most to least recent     go bsn bs bss =-      case B.elemIndex 10 bs of-        Just i -> return (B.tail b,+      case B8.elemIndex '\n' bs of+        Just i -> return (B.tail b, -- tail drops newline                           Just (cleanEnd (B.concat (reverse (a:bss)))))           where             (a,b) = B.splitAt i bs         Nothing ->           do when (bsn >= n) (throwIO LineTooLong)              more <- networkRecv h n-             if B.null more-               then if B.null bs then return (B.empty, Nothing)-                                 else throwIO LineTruncated+             if B.null more -- connection closed+               then if bsn == 0 then return (B.empty, Nothing)+                                else throwIO LineTruncated                else go (bsn + B.length more) more (bs:bss)  +-- | Push a 'ByteString' onto the buffer so that it will be the first+-- bytes to be read on the next receive operation. This could perhaps+-- be useful for putting the unused portion of a 'recv' back into the+-- buffer for future 'recvLine' or 'recv' operations.+putBuf ::+  Connection {- ^ connection         -} ->+  ByteString {- ^ new head of buffer -} ->+  IO ()+putBuf (Connection buf h) bs =+  modifyMVar_ buf (\old -> return $! B.append bs old)++ -- | Remove the trailing @'\\r'@ if one is found. cleanEnd :: ByteString -> ByteString cleanEnd bs-  | B.null bs || B.last bs /= 13 = bs-  | otherwise                    = B.init bs+  | B.null bs || B8.last bs /= '\r' = bs+  | otherwise                       = B.init bs  --- | Send bytes on the network connection.+-- | Send bytes on the network connection. This ensures the whole chunk is+-- transmitted, which might take multiple underlying sends. ----- Throws: 'IOError', 'ProtocolError'-send :: Connection -> ByteString -> IO ()+-- Throws: 'IOError', 'SSL.ProtocolError'+send ::+  Connection {- ^ open connection -} ->+  ByteString {- ^ chunk           -} ->+  IO () send (Connection _ h) = networkSend h  @@ -263,18 +451,19 @@ -- | Initiate a TLS session on the given socket destined for -- the given hostname. When successful an active TLS connection -- is returned with certificate verification successful when--- requested.+-- requested. This function requires that the TLSParams component+-- of 'ConnectionParams' is set. startTls ::-  HostName  {- ^ server hostname  -} ->-  TlsParams {- ^ parameters       -} ->-  Socket    {- ^ connected socket -} ->-  IO SSL    {- ^ connected TLS    -}-startTls host tp s = SSL.withOpenSSL $+  TlsParams {- ^ connection params      -} ->+  String    {- ^ hostname               -} ->+  IO Socket {- ^ socket creation action -} ->+  IO SSL    {- ^ connected TLS          -}+startTls tp hostname mkSocket = SSL.withOpenSSL $   do ctx <- SSL.context       -- configure context      SSL.contextSetCiphers          ctx (tpCipherSuite tp)-     installVerification            ctx host+     installVerification            ctx hostname      SSL.contextSetVerificationMode ctx (verificationMode (tpInsecure tp))      SSL.contextAddOption           ctx SSL.SSL_OP_ALL      SSL.contextRemoveOption        ctx SSL.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS@@ -285,8 +474,13 @@      traverse_ (setupPrivateKey  ctx) (tpClientPrivateKey  tp)       -- add socket to context-     ssl <- SSL.connection ctx s-     SSL.setTlsextHostName ssl host+     -- creation of the socket is delayed until this point to avoid+     -- leaking the file descriptor in the cases of exceptions above.+     ssl <- SSL.connection ctx =<< mkSocket++     -- configure hostname used for certificate validation+     SSL.setTlsextHostName ssl hostname+      SSL.connect ssl       return ssl@@ -309,7 +503,7 @@ setupPrivateKey :: SSLContext -> FilePath -> IO () setupPrivateKey ctx path =   do str <- readFile path -- EX-     key <- PEM.readPrivateKey str PEM.PwNone -- add password support+     key <- PEM.readPrivateKey str PEM.PwNone -- TODO: add password support      SSL.contextSetPrivateKey ctx key  
+ src/Hookup/Socks5.hs view
@@ -0,0 +1,360 @@+{-# Language PatternSynonyms #-}+{-# OPTIONS_GHC -Wall -Wno-missing-pattern-synonym-signatures #-}+{-|+Module      : Hookup.Socks5+Description : SOCKS5 network protocol implementation+Copyright   : (c) Eric Mertens, 2018+License     : ISC+Maintainer  : emertens@gmail.com++This module provides types, parsers, and builders for the messages+used in the SOCKS5 protocol. See <https://tools.ietf.org/html/rfc1928>+-}+module Hookup.Socks5++  ( -- * Client hello message+    ClientHello(..)+  , buildClientHello+  , parseClientHello++  -- * Server hello message+  , ServerHello(..)+  , buildServerHello+  , parseServerHello++  -- * Command request message+  , Request(..)+  , buildRequest+  , parseRequest++  -- * Command response message+  , Response(..)+  , buildResponse+  , parseResponse++  -- * Network address types+  , Address(..)+  , Host(..)++  -- * Authentication methods+  , AuthMethod+      ( AuthNoAuthenticationRequired+      , AuthGssApi+      , AuthUsernamePassword+      , AuthNoAcceptableMethods )++  -- * Commands+  , Command+      ( Connect+      , Bind+      , UdpAssociate )++  -- * Command reply codes+  , CommandReply+      ( CommandReply+      , Succeeded+      , GeneralFailure+      , NotAllowed+      , NetUnreachable+      , HostUnreachable+      , ConnectionRefused+      , TTLExpired+      , CmdNotSupported+      , AddrNotSupported )++  )+  where++import           Control.Monad              (replicateM)+import           Data.Attoparsec.ByteString (Parser)+import           Data.ByteString            (ByteString)+import           Data.ByteString.Builder    (Builder)+import           Data.Word                  (Word8, Word16)+import           Network.Socket             (HostAddress, HostAddress6, PortNumber,+                                             hostAddressToTuple, hostAddress6ToTuple,+                                             tupleToHostAddress, tupleToHostAddress6)+import qualified Data.Attoparsec.ByteString as Parser+import qualified Data.ByteString            as B+import qualified Data.ByteString.Builder    as Builder+import qualified Data.ByteString.Lazy       as L++-- | SOCKS authentication methods+newtype AuthMethod                      = AuthMethod Word8 deriving (Eq, Show)+pattern AuthNoAuthenticationRequired    = AuthMethod 0x00+pattern AuthGssApi                      = AuthMethod 0x01+pattern AuthUsernamePassword            = AuthMethod 0x02+pattern AuthNoAcceptableMethods         = AuthMethod 0xFF++-- | SOCKS client commands+newtype Command                         = Command Word8 deriving (Eq, Show)+pattern Connect                         = Command 1+pattern Bind                            = Command 2+pattern UdpAssociate                    = Command 3++-- | Tags used in the protocol messages for encoded 'Host' values+newtype HostTag                         = HostTag Word8 deriving (Eq, Show)+pattern IPv4Tag                         = HostTag 1+pattern DomainNameTag                   = HostTag 3+pattern IPv6Tag                         = HostTag 4++-- | SOCKS command reply codes+newtype CommandReply                    = CommandReply Word8 deriving (Eq, Show)+pattern Succeeded                       = CommandReply 0+pattern GeneralFailure                  = CommandReply 1+pattern NotAllowed                      = CommandReply 2+pattern NetUnreachable                  = CommandReply 3+pattern HostUnreachable                 = CommandReply 4+pattern ConnectionRefused               = CommandReply 5+pattern TTLExpired                      = CommandReply 6+pattern CmdNotSupported                 = CommandReply 7+pattern AddrNotSupported                = CommandReply 8++-- | Network host and port number+data Address = Address Host PortNumber+  deriving Show++-- | Network host identified by address or domain name.+data Host+  = IPv4 HostAddress      -- ^ IPv4 host address+  | IPv6 HostAddress6     -- ^ IPv6 host address+  | DomainName ByteString -- ^ Domain name (maximum length 255)+  deriving Show+++-- | Initial SOCKS sent by client with proposed list of authentication methods.+data ClientHello = ClientHello+  { cHelloMethods :: [AuthMethod] -- ^ proposed methods (maximum length 255)+  }+  deriving Show++-- | Initial SOCKS sent by server with chosen authentication method.+data ServerHello = ServerHello+  { sHelloMethod  :: AuthMethod+  }+  deriving Show++-- | Client message used to request a network operation from the SOCKS server.+data Request = Request+  { reqCommand :: Command+  , reqAddress :: Address+  }+  deriving Show++-- | Server message used to indicate result of client's request.+data Response = Response+  { rspReply   :: CommandReply+  , rspAddress :: Address+  }+  deriving Show++-- | Transform a 'Builder' into a strict 'ByteString'+runBuilder :: Builder -> ByteString+runBuilder = L.toStrict . Builder.toLazyByteString++------------------------------------------------------------------------++buildCommand :: Command -> Builder+buildCommand (Command c) = Builder.word8 c++parseCommand :: Parser Command+parseCommand = Command <$> Parser.anyWord8++------------------------------------------------------------------------++buildHost :: Host -> Builder+buildHost (IPv4 hostAddr) = buildHostTag IPv4Tag       <> buildHostAddress  hostAddr+buildHost (IPv6 hostAddr) = buildHostTag IPv6Tag       <> buildHostAddress6 hostAddr+buildHost (DomainName dn) = buildHostTag DomainNameTag <> buildDomainName dn++parseHost :: Parser Host+parseHost =+  do tag <- parseHostTag+     case tag of+       IPv4Tag       -> IPv4       <$> parseHostAddress+       IPv6Tag       -> IPv6       <$> parseHostAddress6+       DomainNameTag -> DomainName <$> parseDomainName+       _             -> fail "bad address tag"++------------------------------------------------------------------------++buildAddress :: Address -> Builder+buildAddress (Address host port) = buildHost host <> buildPort port++parseAddress :: Parser Address+parseAddress = Address <$> parseHost <*> parsePort++------------------------------------------------------------------------++buildHostTag :: HostTag -> Builder+buildHostTag (HostTag tag) = Builder.word8 tag++parseHostTag :: Parser HostTag+parseHostTag = HostTag <$> Parser.anyWord8++------------------------------------------------------------------------++buildHostAddress :: HostAddress -> Builder+buildHostAddress hostAddr =+  case hostAddressToTuple hostAddr of+    (a1,a2,a3,a4) -> foldMap Builder.word8 [a1,a2,a3,a4]++parseHostAddress :: Parser HostAddress+parseHostAddress =+  do [a1,a2,a3,a4] <- replicateM 4 Parser.anyWord8+     return $! tupleToHostAddress (a1,a2,a3,a4)++------------------------------------------------------------------------++buildHostAddress6 :: HostAddress6 -> Builder+buildHostAddress6 hostAddr =+  case hostAddress6ToTuple hostAddr of+    (a1,a2,a3,a4,a5,a6,a7,a8) ->+      foldMap Builder.word16BE [a1,a2,a3,a4,a5,a6,a7,a8]++parseHostAddress6 :: Parser HostAddress6+parseHostAddress6 =+  do [a1,a2,a3,a4,a5,a6,a7,a8] <- replicateM 8 parseWord16BE+     return $! tupleToHostAddress6 (a1,a2,a3,a4,a5,a6,a7,a8)++------------------------------------------------------------------------++buildDomainName :: ByteString -> Builder+buildDomainName bs+  | B.length bs < 256 = Builder.word8 (fromIntegral (B.length bs)) <>+                        Builder.byteString bs+  | otherwise = error "SOCKS5 domain name too long"++parseDomainName :: Parser ByteString+parseDomainName =+  do len <- Parser.anyWord8+     Parser.take (fromIntegral len)++------------------------------------------------------------------------++buildPort :: PortNumber -> Builder+buildPort port = Builder.word16BE (fromIntegral port)++parsePort :: Parser PortNumber+parsePort = fromIntegral <$> parseWord16BE++------------------------------------------------------------------------++buildVersion :: Builder+buildVersion = Builder.word8 5++parseVersion :: Parser ()+parseVersion = () <$ Parser.word8 5++------------------------------------------------------------------------++buildAuthMethod :: AuthMethod -> Builder+buildAuthMethod (AuthMethod x) = Builder.word8 x++parseAuthMethod :: Parser AuthMethod+parseAuthMethod = AuthMethod <$> Parser.anyWord8++------------------------------------------------------------------------++buildReply :: CommandReply -> Builder+buildReply (CommandReply x) = Builder.word8 x++parseReply :: Parser CommandReply+parseReply = CommandReply <$> Parser.anyWord8++------------------------------------------------------------------------++buildReserved :: Builder+buildReserved = Builder.word8 0++parseReserved :: Parser ()+parseReserved = () <$ Parser.anyWord8++------------------------------------------------------------------------++-- | Build a list of buildable things prefixing the length of the list+-- as a single byte. The list must not be longer than 255 elements.+buildListOf :: (a -> Builder) -> [a] -> Builder+buildListOf builder xs+  | length xs < 256 = Builder.word8 (fromIntegral (length xs)) <>+                      foldMap builder xs+  | otherwise       = error "buildListOf: list too long"++-- | Parse a list of parsable things where the length of the list+-- is encoded as a single byte before the items to be parsed.+parseListOf :: Parser a -> Parser [a]+parseListOf parser =+  do n <- Parser.anyWord8+     replicateM (fromIntegral n) parser++------------------------------------------------------------------------++buildClientHello :: ClientHello -> ByteString+buildClientHello msg =+  runBuilder $+  buildVersion <>+  buildListOf buildAuthMethod (cHelloMethods msg)++parseClientHello :: Parser ClientHello+parseClientHello =+  ClientHello+    <$  parseVersion+    <*> parseListOf parseAuthMethod++------------------------------------------------------------------------++buildServerHello :: ServerHello -> ByteString+buildServerHello msg =+  runBuilder $+  buildVersion <>+  buildAuthMethod (sHelloMethod msg)++parseServerHello :: Parser ServerHello+parseServerHello =+  ServerHello+    <$  parseVersion+    <*> parseAuthMethod++------------------------------------------------------------------------++buildRequest :: Request -> ByteString+buildRequest req =+  runBuilder $+  buildVersion                    <>+  buildCommand  (reqCommand  req) <>+  buildReserved                   <>+  buildAddress  (reqAddress  req)++parseRequest :: Parser Request+parseRequest =+  Request+    <$  parseVersion+    <*> parseCommand+    <*  parseReserved+    <*> parseAddress++------------------------------------------------------------------------++buildResponse :: Response -> ByteString+buildResponse msg =+  runBuilder $+  buildVersion                  <>+  buildReply   (rspReply   msg) <>+  buildReserved                 <>+  buildAddress (rspAddress msg)++parseResponse :: Parser Response+parseResponse =+  Response+    <$  parseVersion+    <*> parseReply+    <*  parseReserved+    <*> parseAddress++------------------------------------------------------------------------++-- | Match a 16-bit, big-endian word.+parseWord16BE :: Parser Word16+parseWord16BE =+  do hi <- Parser.anyWord8+     lo <- Parser.anyWord8+     return $! fromIntegral hi * 0x100 + fromIntegral lo