packages feed

hookup 0.4 → 0.5

raw patch · 6 files changed

+294/−88 lines, 6 filesdep +asyncdep +stmdep ~networkPVP ok

version bump matches the API change (PVP)

Dependencies added: async, stm

Dependency ranges changed: network

API changes (from Hackage documentation)

+ Hookup: upgradeTls :: TlsParams -> String -> Connection -> IO ()
+ Hookup.OpenSSL: getPubKeyDer :: X509 -> IO ByteString
+ Hookup.OpenSSL: installVerification :: SSLContext -> String -> IO ()
+ Hookup.OpenSSL: withDefaultPassword :: SSLContext -> Maybe ByteString -> IO a -> IO a
- Hookup: TlsParams :: Maybe FilePath -> Maybe FilePath -> PemPasswordSupply -> Maybe FilePath -> String -> Bool -> TlsParams
+ Hookup: TlsParams :: Maybe FilePath -> Maybe FilePath -> Maybe ByteString -> Maybe FilePath -> String -> Bool -> TlsParams
- Hookup: [tpClientPrivateKeyPassword] :: TlsParams -> PemPasswordSupply
+ Hookup: [tpClientPrivateKeyPassword] :: TlsParams -> Maybe ByteString
- Hookup: getClientCertificate :: Connection -> Maybe X509
+ Hookup: getClientCertificate :: Connection -> IO (Maybe X509)

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # Revision history for hookup +## 0.5++* Don't use `AI_ADDRCONFIG` flag+* Support all client-side certificates supported by OpenSSL (rather than just RSA/DSA)+* Add support for STARTTLS+ ## 0.4  * Added ability to specify TLS private key password
+ cbits/pem_password_cb.c view
@@ -0,0 +1,49 @@+#include <stdlib.h>+#include <string.h>++struct CStringLen+{+    char const* ptr;+    int len;+};++void *+hookup_new_userdata(char const* ptr, int len)+{+    struct CStringLen *result = malloc(sizeof *result);++    // The null/error case is handled in hookup_pem_passwd_cb+    if (NULL != result) {+        result->ptr = ptr;+        result->len = len;+    }++    return result;+}++void+hookup_free_userdata(void *ud)+{+        free(ud);+}++int+hookup_pem_passwd_cb(char *buf, int size, int rwflag, void *userdata)+{+    struct CStringLen *password = userdata;++    // hookup_new_userdata failed, so we fail.+    if (NULL == password) { return -1; }++    // password requested when none was provided, so we fail.+    if (0 > password->len) { return -1; }++    // OpenSSL says to truncate the password if it's too long+    if (password->len < size) {+        size = password->len;+    }++    memcpy(buf, password->ptr, size);++    return size;+}
hookup.cabal view
@@ -1,5 +1,6 @@+cabal-version:       2.2 name:                hookup-version:             0.4+version:             0.5 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)@@ -7,14 +8,13 @@ license-file:        LICENSE author:              Eric Mertens maintainer:          emertens@gmail.com-copyright:           2016 Eric Mertens+copyright:           2016-2020 Eric Mertens category:            Network build-type:          Simple extra-source-files:  ChangeLog.md homepage:            https://github.com/glguy/irc-core bug-reports:         https://github.com/glguy/irc-core/issues-tested-with:         GHC==8.0.2-cabal-version:       >=1.10+tested-with:         GHC==8.10.1  source-repository head   type: git@@ -22,14 +22,26 @@   branch: v2  library-  exposed-modules:     Hookup-  other-modules:       Hookup.OpenSSL,-                       Hookup.Socks5-  build-depends:       base                  >=4.11 && <4.15,-                       network               >=2.6  && <3.2,-                       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   default-language:    Haskell2010++  exposed-modules:+    Hookup+    Hookup.OpenSSL++  other-modules:+    Hookup.Socks5+    Hookup.Concurrent++  c-sources:+    cbits/pem_password_cb.c++  build-depends:+    base                  >=4.11 && <4.15,+    async                 ^>=2.2,+    stm                   ^>=2.5,+    network               >=3.0  && <3.2,+    bytestring            >=0.10 && <0.11,+    attoparsec            >=0.13 && <0.14,+    HsOpenSSL             >=0.11.2.3 && <0.12,+    HsOpenSSL-x509-system >=0.1  && <0.2,
src/Hookup.hs view
@@ -1,3 +1,4 @@+{-# Language BlockArguments #-} {-| Module      : Hookup Description : Network connections generalized over TLS and SOCKS@@ -30,6 +31,7 @@   connect,   connectWithSocket,   close,+  upgradeTls,    -- * Reading and writing data   recv,@@ -60,6 +62,8 @@   , getPeerPubkeyFingerprintSha512   ) where +import           Control.Concurrent.Async+import           Control.Concurrent.STM import           Control.Concurrent import           Control.Exception import           Control.Monad@@ -69,6 +73,9 @@ import qualified Data.ByteString.Char8 as B8 import           Data.Foldable import           Data.List (intercalate, partition)+import           Data.Maybe (fromMaybe)+import           Foreign.C.String (withCStringLen)+import           Foreign.Ptr (nullPtr) import           Network.Socket (AddrInfo, HostName, PortNumber, SockAddr, Socket, Family) import qualified Network.Socket as Socket import qualified Network.Socket.ByteString as SocketB@@ -83,10 +90,10 @@ import           Data.Attoparsec.ByteString (Parser) import qualified Data.Attoparsec.ByteString as Parser -import           Hookup.OpenSSL (installVerification, getPubKeyDer)+import           Hookup.Concurrent (concurrentAttempts)+import           Hookup.OpenSSL import           Hookup.Socks5 - -- | Parameters for 'connect'. -- -- Common defaults for fields: 'defaultFamily', 'defaultTlsParams'@@ -113,13 +120,12 @@   , spPort :: PortNumber -- ^ SOCKS server port   } - -- | TLS connection parameters. These parameters are passed to -- OpenSSL when making a secure connection. 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+  , tpClientPrivateKeyPassword :: Maybe ByteString -- ^ 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'@@ -180,7 +186,7 @@ defaultTlsParams = TlsParams   { tpClientCertificate  = Nothing   , tpClientPrivateKey   = Nothing-  , tpClientPrivateKeyPassword = PEM.PwNone+  , tpClientPrivateKeyPassword = Nothing   , tpServerCertificate  = Nothing -- use system provided CAs   , tpCipherSuite        = "HIGH"   , tpInsecure           = False@@ -259,12 +265,15 @@      let pairs = interleaveAddressFamilies (matchBindAddrs mbSrc dst)      when (null pairs)        (throwIO (HostnameResolutionFailure h "No source/destination address family match"))-     attempt pairs+     res <- concurrentAttempts connAttemptDelay Socket.close (uncurry connectToAddrInfo <$> pairs)+     case res of+       Left es -> throwIO (ConnectionFailure [ioe | e <- es, Just ioe <- [fromException e]])+       Right s -> pure s  hints :: AddrInfo hints = Socket.defaultHints   { Socket.addrSocketType = Socket.Stream-  , Socket.addrFlags      = [Socket.AI_ADDRCONFIG, Socket.AI_NUMERICSERV]+  , Socket.addrFlags      = [Socket.AI_NUMERICSERV]   }  resolve :: Maybe PortNumber -> HostName -> IO [AddrInfo]@@ -294,36 +303,6 @@ connAttemptDelay :: Int connAttemptDelay = 150 * 1000 -- 150ms -attempt ::-  [(Maybe SockAddr, AddrInfo)] {- ^ candidate AddrInfos -} ->-  IO Socket         {- ^ connected socket    -}-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-       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@@ -404,7 +383,10 @@ -- | A connection to a network service along with its read buffer -- used for line-oriented protocols. The connection could be a plain -- network connection, SOCKS connected, or TLS.-data Connection = Connection (MVar ByteString) NetworkHandle+data Connection =+  Connection+  {-# UNPACK #-} !(MVar ByteString)+  {-# UNPACK #-} !(MVar NetworkHandle)  -- | Open network connection to TCP service specified by -- the given parameters.@@ -418,8 +400,7 @@   IO Connection    {- ^ open connection -} connect params =   do h <- openNetworkHandle params (openSocket params)-     b <- newMVar B.empty-     return (Connection b h)+     Connection <$> newMVar B.empty <*> newMVar h  -- | Create a new 'Connection' using an already connected socket. -- This will attempt to start TLS if configured but will ignore@@ -433,14 +414,13 @@   IO Connection    {- ^ open connection  -} connectWithSocket params sock =   do h <- openNetworkHandle params (return sock)-     b <- newMVar B.empty-     return (Connection b h)+     Connection <$> newMVar B.empty <*> newMVar h  -- | Close network connection. close ::   Connection {- ^ open connection -} ->   IO ()-close (Connection _ h) = closeNetworkHandle h+close (Connection _ m) = withMVar m $ \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@@ -451,11 +431,13 @@   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+recv (Connection bufVar hVar) n =+  modifyMVar bufVar $ \bufChunk ->+  do if B.null bufChunk+       then do h <- readMVar hVar+               bs <- networkRecv h n+               return (B.empty, bs)+       else return (B.empty, bufChunk)  -- | Receive a line from the network connection. Both -- @"\\r\\n"@ and @"\\n"@ are recognized.@@ -472,14 +454,15 @@   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 []+recvLine (Connection bufVar hVar) n =+  modifyMVar bufVar $ \bs ->+    do h <- readMVar hVar+       go h (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 =+    go h bsn bs bss =       case B8.elemIndex '\n' bs of         Just i -> return (B.tail b, -- tail drops newline                           Just (cleanEnd (B.concat (reverse (a:bss)))))@@ -491,7 +474,7 @@              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)+               else go h (bsn + B.length more) more (bs:bss)   -- | Push a 'ByteString' onto the buffer so that it will be the first@@ -502,8 +485,8 @@   Connection {- ^ connection         -} ->   ByteString {- ^ new head of buffer -} ->   IO ()-putBuf (Connection buf h) bs =-  modifyMVar_ buf (\old -> return $! B.append bs old)+putBuf (Connection bufVar _) bs =+  modifyMVar_ bufVar (\old -> return $! B.append bs old)   -- | Remove the trailing @'\\r'@ if one is found.@@ -521,9 +504,25 @@   Connection {- ^ open connection -} ->   ByteString {- ^ chunk           -} ->   IO ()-send (Connection _ h) = networkSend h+send (Connection _ hVar) bs =+  do h <- readMVar hVar+     networkSend h bs  +upgradeTls ::+  TlsParams {- ^ connection params -} ->+  String {- ^ hostname -} ->+  Connection ->+  IO ()+upgradeTls tp hostname (Connection bufVar hVar) =+  modifyMVar_ bufVar $ \buf ->+  modifyMVar  hVar   $ \h ->+  case h of+    SSL{} -> return (h, buf)+    Socket s ->+      do (cert, ssl) <- startTls tp hostname (pure s)+         return (SSL cert ssl, B.empty)+ ------------------------------------------------------------------------  @@ -547,11 +546,15 @@      SSL.contextAddOption           ctx SSL.SSL_OP_ALL      SSL.contextRemoveOption        ctx SSL.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS +      -- configure certificates      setupCaCertificates ctx (tpServerCertificate tp)      clientCert <- traverse (setupCertificate ctx) (tpClientCertificate tp)-     traverse_ (setupPrivateKey ctx (tpClientPrivateKeyPassword tp)) (tpClientPrivateKey tp) +     for_ (tpClientPrivateKey tp) $ \path ->+       withDefaultPassword ctx (tpClientPrivateKeyPassword tp) $+         SSL.contextSetPrivateKeyFile ctx path+      -- add socket to context      -- creation of the socket is delayed until this point to avoid      -- leaking the file descriptor in the cases of exceptions above.@@ -564,12 +567,11 @@       return (clientCert, ssl) - setupCaCertificates :: SSLContext -> Maybe FilePath -> IO () setupCaCertificates ctx mbPath =   case mbPath of     Nothing   -> contextLoadSystemCerts ctx-    Just path -> SSL.contextSetCAFile ctx path+    Just path -> withDefaultPassword ctx Nothing (SSL.contextSetCAFile ctx path)   setupCertificate :: SSLContext -> FilePath -> IO X509@@ -579,13 +581,6 @@      pure x509  -setupPrivateKey :: SSLContext -> PEM.PemPasswordSupply -> FilePath -> IO ()-setupPrivateKey ctx password path =-  do str <- readFile path -- EX-     key <- PEM.readPrivateKey str password-     SSL.contextSetPrivateKey ctx key-- verificationMode :: Bool {- ^ insecure -} -> SSL.VerificationMode verificationMode insecure   | insecure  = SSL.VerifyNone@@ -597,17 +592,19 @@  -- | Get peer certificate if one exists. getPeerCertificate :: Connection -> IO (Maybe X509.X509)-getPeerCertificate (Connection _ h) =+getPeerCertificate (Connection _ hVar) =+  withMVar hVar $ \h ->   case h of     Socket{} -> return Nothing     SSL _ ssl -> SSL.getPeerCertificate ssl  -- | Get peer certificate if one exists.-getClientCertificate :: Connection -> Maybe X509.X509-getClientCertificate (Connection _ h) =-  case h of-    Socket{} -> Nothing-    SSL c _  -> c+getClientCertificate :: Connection -> IO (Maybe X509.X509)+getClientCertificate (Connection _ hVar) =+  do h <- readMVar hVar+     return $ case h of+                Socket{} -> Nothing+                SSL c _  -> c  getPeerCertFingerprintSha1 :: Connection -> IO (Maybe ByteString) getPeerCertFingerprintSha1 = getPeerCertFingerprint "sha1"
+ src/Hookup/Concurrent.hs view
@@ -0,0 +1,99 @@+{-# Language BlockArguments, ScopedTypeVariables #-}+{-|+Module      : Hookup.Concurrent+Description : Concurrently run actions until one succeeds or all fail+Copyright   : (c) Eric Mertens, 2020+License     : ISC+Maintainer  : emertens@gmail.com++-}+module Hookup.Concurrent (concurrentAttempts) where++import Control.Concurrent (forkIO, throwTo)+import Control.Concurrent.Async (Async, AsyncCancelled(..), async, asyncThreadId, cancel, waitCatch, waitCatchSTM)+import Control.Concurrent.STM (STM, atomically, check, orElse, readTVar, registerDelay, retry)+import Control.Exception (SomeException, finally, mask_, onException)+import Control.Monad (join)+import Data.Foldable (for_)++concurrentAttempts ::+  Int {- ^ microsecond delay between attempts -} ->+  (a -> IO ()) {- ^ release unneeded success -} ->+  [IO a] {- ^ ordered list of attempts -} ->+  IO (Either [SomeException] a)+concurrentAttempts delay release actions =+  let st = St { threads = [],+                errors = [],+                work = actions,+                delay = delay,+                clean = release,+                readySTM = retry }+  in mask_ (loop st)++data St a = St+  { threads :: [Async a]+  , errors  :: [SomeException]+  , work    :: [IO a]+  , delay   :: !Int+  , clean   :: a -> IO ()+  , readySTM :: STM ()+  }++type Answer a = IO (Either [SomeException] a)++-- | Main event loop for concurrent attempt system+loop :: forall a. St a -> Answer a+loop st = if null (threads st) then nothingRunning st else waitForEvent st++-- | No threads are active, either start a new thread or return the complete error list+nothingRunning :: St a -> Answer a+nothingRunning st =+  case work st of+    [] -> pure (Left (errors st))+    x:xs -> start x st{work = xs}++-- | Start a new thread for the given attempt+start :: IO a -> St a -> Answer a+start x st =+  do thread <- async x+     ready <- if null (work st) then pure retry else startTimer (delay st)+     loop st { threads = thread : threads st, readySTM = ready }++-- Nothing to do but wait for a thread to finish or the timer to fire+waitForEvent :: St a -> Answer a+waitForEvent st = join (atomically (finish st [] (threads st))+                  `onException` cleanup (clean st) (threads st))++-- Search for an event out of the active threads and timer+finish :: St a -> [Async a] -> [Async a] -> STM (Answer a)+finish st threads' [] = fresh st+finish st threads' (t:ts) = finish1 st (threads' ++ ts) t+                   `orElse` finish st (t:threads') ts++-- Handle a thread completion event+finish1 :: St a -> [Async a] -> Async a -> STM (Answer a)+finish1 st threads' t =+      do res <- waitCatchSTM t+         pure case res of+           Right s -> Right s <$ cleanup (clean st) threads'+           Left e -> loop st { errors = e : errors st, threads = threads'}++-- Handle a new thread timer event+fresh :: St a -> STM (Answer a)+fresh st =+  case work st of+    [] -> retry+    x:xs -> start x st{work = xs} <$ readySTM st++-- | Create an STM action that only succeeds after at least 'n' microseconds have passed.+startTimer :: Int -> IO (STM ())+startTimer n =+  do v <- registerDelay n+     pure (check =<< readTVar v)++-- non-blocking cancelation of the remaining threads+cleanup :: (a -> IO ()) -> [Async a] -> IO ()+cleanup release xs =+  () <$ forkIO do for_ xs \x -> throwTo (asyncThreadId x) AsyncCancelled+                  for_ xs \x -> do res <- waitCatch x+                                   for_ res release
src/Hookup/OpenSSL.hsc view
@@ -15,16 +15,59 @@ #error "OpenSSL 1.0.2 or later is required. This version was released in Jan 2015 and adds hostname verification" #endif -module Hookup.OpenSSL (installVerification, getPubKeyDer) where+module Hookup.OpenSSL (withDefaultPassword, installVerification, getPubKeyDer) where +import           Control.Exception (bracket, bracket_) import           Control.Monad (unless)-import           Foreign.C (CString(..), CSize(..), CUInt(..), CInt(..), withCStringLen)-import           Foreign.Ptr (Ptr, castPtr, nullPtr)+import           Foreign.C (CStringLen, CString(..), CSize(..), CUInt(..), CInt(..), withCStringLen, CChar(..))+import           Foreign.Ptr (FunPtr, Ptr, castPtr, nullPtr, nullFunPtr)+import           Foreign.StablePtr (StablePtr, deRefStablePtr, castPtrToStablePtr) import           Foreign.Marshal (with) import           OpenSSL.Session (SSLContext, SSLContext_, withContext) import           OpenSSL.X509 (withX509Ptr, X509, X509_) import           Data.ByteString (ByteString) import qualified Data.ByteString.Internal as B+import qualified Data.ByteString.Unsafe as Unsafe++------------------------------------------------------------------------+-- Bindings to password callback+------------------------------------------------------------------------++foreign import ccall unsafe "hookup_new_userdata"+  hookup_new_userdata :: CString -> CInt -> IO (Ptr ())++foreign import ccall unsafe "hookup_free_userdata"+  hookup_free_userdata :: Ptr () -> IO ()++foreign import ccall "&hookup_pem_passwd_cb"+  hookup_pem_passwd_cb :: FunPtr PemPasswdCb++-- int pem_passwd_cb(char *buf, int size, int rwflag, void *userdata);+type PemPasswdCb = Ptr CChar -> CInt -> CInt -> Ptr () -> IO CInt++-- void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_password_cb *cb);+foreign import ccall unsafe "SSL_CTX_set_default_passwd_cb"+  sslCtxSetDefaultPasswdCb :: Ptr SSLContext_ -> FunPtr PemPasswdCb -> IO ()++-- void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u);+foreign import ccall unsafe "SSL_CTX_set_default_passwd_cb_userdata"+  sslCtxSetDefaultPasswdCbUserdata ::+    Ptr SSLContext_ -> Ptr a -> IO ()++withDefaultPassword :: SSLContext -> Maybe ByteString -> IO a -> IO a+withDefaultPassword ctx mbBs m =+  withCPassword mbBs $ \ptr len ->+  bracket (hookup_new_userdata ptr len) hookup_free_userdata $ \ud ->+  bracket_ (setup hookup_pem_passwd_cb ud) (setup nullFunPtr nullPtr) m++  where+  withCPassword Nothing k = k nullPtr (-1)+  withCPassword (Just bs) k = Unsafe.unsafeUseAsCStringLen bs $ \(ptr, len) -> k ptr (fromIntegral len)++  setup cb ud =+    withContext ctx $ \ctxPtr ->+    do sslCtxSetDefaultPasswdCb         ctxPtr cb+       sslCtxSetDefaultPasswdCbUserdata ctxPtr ud  ------------------------------------------------------------------------ -- Bindings to hostname verification interface