packages feed

hookup 0.3.1.0 → 0.4

raw patch · 3 files changed

+119/−52 lines, 3 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Hookup: [cpFamily] :: ConnectionParams -> Family
- Hookup: defaultFamily :: Family
+ Hookup: PwBS :: ByteString -> PemPasswordSupply
+ Hookup: PwCallback :: PemPasswordCallback -> PemPasswordSupply
+ Hookup: PwNone :: PemPasswordSupply
+ Hookup: PwStr :: String -> PemPasswordSupply
+ Hookup: PwTTY :: PemPasswordSupply
+ Hookup: [cpBind] :: ConnectionParams -> Maybe HostName
+ Hookup: [tpClientPrivateKeyPassword] :: TlsParams -> PemPasswordSupply
+ Hookup: data PemPasswordSupply
- Hookup: ConnectionParams :: Family -> HostName -> PortNumber -> Maybe SocksParams -> Maybe TlsParams -> ConnectionParams
+ Hookup: ConnectionParams :: HostName -> PortNumber -> Maybe SocksParams -> Maybe TlsParams -> Maybe HostName -> ConnectionParams
- Hookup: TlsParams :: Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> String -> Bool -> TlsParams
+ Hookup: TlsParams :: Maybe FilePath -> Maybe FilePath -> PemPasswordSupply -> Maybe FilePath -> String -> Bool -> TlsParams

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # Revision history for hookup +## 0.4++* Added ability to specify TLS private key password+* Replace protocol family selection with more general bind hostname selection+* Implement staggered, concurrent connection strategy based on RFC 8305+ ## 0.3.1.0 -- 2020-02  * Added `getClientCertificate`
hookup.cabal view
@@ -1,5 +1,5 @@ name:                hookup-version:             0.3.1.0+version:             0.4 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)@@ -25,7 +25,7 @@   exposed-modules:     Hookup   other-modules:       Hookup.OpenSSL,                        Hookup.Socks5-  build-depends:       base                  >=4.11 && <4.14,+  build-depends:       base                  >=4.11 && <4.15,                        network               >=2.6  && <3.2,                        bytestring            >=0.10 && <0.11,                        attoparsec            >=0.13 && <0.14,
src/Hookup.hs view
@@ -41,7 +41,7 @@   ConnectionParams(..),   SocksParams(..),   TlsParams(..),-  defaultFamily,+  PEM.PemPasswordSupply(..),   defaultTlsParams,  @@ -68,8 +68,8 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8 import           Data.Foldable-import           Data.List (intercalate)-import           Network.Socket (Socket, AddrInfo, PortNumber, HostName, Family)+import           Data.List (intercalate, partition)+import           Network.Socket (AddrInfo, HostName, PortNumber, SockAddr, Socket, Family) import qualified Network.Socket as Socket import qualified Network.Socket.ByteString as SocketB import           OpenSSL.Session (SSL, SSLContext)@@ -91,25 +91,22 @@ -- -- 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.+--+-- The binding hostname can be used to force the connect to use a particular+-- interface or IP protocol version. data ConnectionParams = ConnectionParams-  { cpFamily :: Family           -- ^ IP Protocol family (default 'AF_UNSPEC')-  , cpHost  :: HostName          -- ^ Destination host+  { cpHost  :: HostName          -- ^ Destination host   , cpPort  :: PortNumber        -- ^ Destination TCP port   , cpSocks :: Maybe SocksParams -- ^ Optional SOCKS parameters   , cpTls   :: Maybe TlsParams   -- ^ Optional TLS parameters+  , cpBind  :: Maybe HostName    -- ^ Source address to bind   } - -- | SOCKS connection parameters data SocksParams = SocksParams   { spHost :: HostName   -- ^ SOCKS server host@@ -122,6 +119,7 @@ data TlsParams = TlsParams   { tpClientCertificate  :: Maybe FilePath -- ^ Path to client certificate   , tpClientPrivateKey   :: Maybe FilePath -- ^ Path to client private key+  , tpClientPrivateKeyPassword :: PEM.PemPasswordSupply -- ^ Private key decryption password   , tpServerCertificate  :: Maybe FilePath -- ^ Path to CA certificate bundle   , tpCipherSuite        :: String -- ^ OpenSSL cipher suite name (e.g. @\"HIGH\"@)   , tpInsecure           :: Bool -- ^ Disables certificate checking when 'True'@@ -176,16 +174,13 @@       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+  , tpClientPrivateKeyPassword = PEM.PwNone   , tpServerCertificate  = Nothing -- use system provided CAs   , tpCipherSuite        = "HIGH"   , tpInsecure           = False@@ -200,9 +195,9 @@ openSocket :: ConnectionParams -> IO Socket openSocket params =   case cpSocks params of-    Nothing -> openSocket' (cpFamily params) (cpHost params) (cpPort params)+    Nothing -> openSocket' (cpHost params) (cpPort params) (cpBind params)     Just sp ->-      do sock <- openSocket' (cpFamily params) (spHost sp) (spPort sp)+      do sock <- openSocket' (spHost sp) (spPort sp) (cpBind params)          (sock <$ socksConnect sock (cpHost params) (cpPort params))            `onException` Socket.close sock @@ -253,46 +248,112 @@     (throwIO (SocksError (rspReply response)))  -openSocket' :: Family -> HostName -> PortNumber -> IO Socket-openSocket' family h p =-  do let hints = Socket.defaultHints-           { Socket.addrFamily     = family-           , Socket.addrSocketType = Socket.Stream-           , Socket.addrFlags      = [Socket.AI_ADDRCONFIG-                                     ,Socket.AI_NUMERICSERV]-           }-     res <- try (Socket.getAddrInfo (Just hints) (Just h) (Just (show p)))+openSocket' ::+  HostName       {- ^ destination      -} ->+  PortNumber     {- ^ destination port -} ->+  Maybe HostName {- ^ source           -} ->+  IO Socket      {- ^ connected socket -}+openSocket' h p mbBind =+  do mbSrc <- traverse (resolve Nothing) mbBind+     dst   <- resolve (Just p) h+     let pairs = interleaveAddressFamilies (matchBindAddrs mbSrc dst)+     when (null pairs)+       (throwIO (HostnameResolutionFailure h "No source/destination address family match"))+     attempt pairs++hints :: AddrInfo+hints = Socket.defaultHints+  { Socket.addrSocketType = Socket.Stream+  , Socket.addrFlags      = [Socket.AI_ADDRCONFIG, Socket.AI_NUMERICSERV]+  }++resolve :: Maybe PortNumber -> HostName -> IO [AddrInfo]+resolve mbPort host =+  do res <- try (Socket.getAddrInfo (Just hints) (Just host) (show<$>mbPort))      case res of-       Right ais -> attemptConnections [] ais-       Left  ioe+       Right ais -> return ais+       Left ioe          | isDoesNotExistError ioe ->-             throwIO (HostnameResolutionFailure h (ioeGetErrorString ioe))+             throwIO (HostnameResolutionFailure host (ioeGetErrorString ioe))          | otherwise -> throwIO ioe -- unexpected +-- | When no bind address is specified return the full list of destination+-- addresses with no bind address specified.+--+-- When bind addresses are specified return a subset of the destination list+-- matched up with the first address from the bind list that has the+-- correct address family.+matchBindAddrs :: Maybe [AddrInfo] -> [AddrInfo] -> [(Maybe SockAddr, AddrInfo)]+matchBindAddrs Nothing    dst = [ (Nothing, x) | x <- dst ]+matchBindAddrs (Just src) dst =+  [ (Just (Socket.addrAddress s), d)+  | d <- dst+  , let ss = [s | s <- src, Socket.addrFamily d == Socket.addrFamily s]+  , s <- take 1 ss ] --- | 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 -} ->+connAttemptDelay :: Int+connAttemptDelay = 150 * 1000 -- 150ms++attempt ::+  [(Maybe SockAddr, AddrInfo)] {- ^ candidate AddrInfos -} ->   IO Socket         {- ^ connected socket    -}-attemptConnections exs [] = throwIO (ConnectionFailure exs)-attemptConnections exs (ai:ais) =-  do res <- try (connectToAddrInfo ai)+attempt xs =+  do comm <- newEmptyMVar++     let mkThread i (mbSrc, ai) =+           forkIOWithUnmask $ \unmask ->+           unmask $+           do threadDelay (connAttemptDelay * i)+              putMVar comm =<< try (connectToAddrInfo mbSrc ai)++     bracket (zipWithM mkThread [0..] xs)+             (traverse_ killThread)+             (\_ -> gather (length xs) [] comm)++-- Either gather all of the errors possible and throw an exception or+-- return the first successful socket.+gather ::+  Int {- ^ potential errors remaining -} ->+  [IOError] {- ^ errors gathered so far -} ->+  MVar (Either IOError Socket) ->+  IO Socket+gather 0 exs _ = throwIO (ConnectionFailure exs)+gather n exs comm =+  do res <- takeMVar comm      case res of-       Left ex -> attemptConnections (ex:exs) ais-       Right s -> return s+       Right s -> pure s+       Left ex -> gather (n-1) (ex:exs) comm +-- | Alternate list of addresses between IPv6 and other (IPv4) addresses.+interleaveAddressFamilies :: [(Maybe SockAddr, AddrInfo)] -> [(Maybe SockAddr, AddrInfo)]+interleaveAddressFamilies xs = interleave sixes others+  where+    (sixes, others) = partition is6 xs+    is6 x = Socket.AF_INET6 == Socket.addrFamily (snd x)++    interleave (x:xs) (y:ys) = x : y : interleave xs ys+    interleave []     ys     = ys+    interleave xs     []     = xs+ -- | 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)+connectToAddrInfo :: Maybe SockAddr -> AddrInfo -> IO Socket+connectToAddrInfo mbSrc info+  = bracketOnError (socket' info) Socket.close $ \s ->+    do traverse_ (bind' s) mbSrc+       Socket.connect s (Socket.addrAddress info)+       pure s +-- | A version of 'Socket.bind' that doesn't bother binding on the wildcard+-- address. The effect of binding on a wildcard address in this library+-- is to pick an address family. Because of the matching done earlier this+-- is unnecessary for client connections and causes a local port to be+-- unnecessarily fixed early.+bind' :: Socket -> SockAddr -> IO ()+bind' _ (Socket.SockAddrInet _ 0) = pure ()+bind' _ (Socket.SockAddrInet6 _ _ (0,0,0,0) _) = pure ()+bind' s a = Socket.bind s a+ -- | Open a 'Socket' using the parameters from an 'AddrInfo' socket' :: AddrInfo -> IO Socket socket' ai =@@ -489,7 +550,7 @@      -- configure certificates      setupCaCertificates ctx (tpServerCertificate tp)      clientCert <- traverse (setupCertificate ctx) (tpClientCertificate tp)-     traverse_ (setupPrivateKey  ctx) (tpClientPrivateKey  tp)+     traverse_ (setupPrivateKey ctx (tpClientPrivateKeyPassword tp)) (tpClientPrivateKey tp)       -- add socket to context      -- creation of the socket is delayed until this point to avoid@@ -518,10 +579,10 @@      pure x509  -setupPrivateKey :: SSLContext -> FilePath -> IO ()-setupPrivateKey ctx path =+setupPrivateKey :: SSLContext -> PEM.PemPasswordSupply -> FilePath -> IO ()+setupPrivateKey ctx password path =   do str <- readFile path -- EX-     key <- PEM.readPrivateKey str PEM.PwNone -- TODO: add password support+     key <- PEM.readPrivateKey str password      SSL.contextSetPrivateKey ctx key