diff --git a/.github/workflows/build-arm64-main.yaml b/.github/workflows/build-arm64-main.yaml new file mode 100644 index 0000000..4c35da9 --- /dev/null +++ b/.github/workflows/build-arm64-main.yaml @@ -0,0 +1,18 @@ +name: main arm64 + +on: + push: + branches: + - main + + workflow_dispatch: + branches: + - main + +jobs: + call-workflow: + uses: layer-3-communications/.github/.github/workflows/build-haskell.yaml@main + with: + build-arch: arm64 + slack-name: ${{ github.ref_name }} + secrets: inherit diff --git a/.github/workflows/build-arm64-pr.yaml b/.github/workflows/build-arm64-pr.yaml new file mode 100644 index 0000000..7385322 --- /dev/null +++ b/.github/workflows/build-arm64-pr.yaml @@ -0,0 +1,14 @@ +name: pull request arm64 + +on: + pull_request: + branches: + - "*" + +jobs: + call-workflow: + uses: layer-3-communications/.github/.github/workflows/build-haskell.yaml@main + with: + build-arch: arm64 + slack-name: ${{ github.event.pull_request.head.ref }} + secrets: inherit diff --git a/.github/workflows/build-x64-main.yaml b/.github/workflows/build-x64-main.yaml new file mode 100644 index 0000000..7277f20 --- /dev/null +++ b/.github/workflows/build-x64-main.yaml @@ -0,0 +1,18 @@ +name: main x64 + +on: + push: + branches: + - main + + workflow_dispatch: + branches: + - main + +jobs: + call-workflow: + uses: layer-3-communications/.github/.github/workflows/build-haskell.yaml@main + with: + build-arch: x64 + slack-name: ${{ github.ref_name }} + secrets: inherit diff --git a/.github/workflows/build-x64-pr.yaml b/.github/workflows/build-x64-pr.yaml new file mode 100644 index 0000000..a82dd19 --- /dev/null +++ b/.github/workflows/build-x64-pr.yaml @@ -0,0 +1,14 @@ +name: pull request x64 + +on: + pull_request: + branches: + - "*" + +jobs: + call-workflow: + uses: layer-3-communications/.github/.github/workflows/build-haskell.yaml@main + with: + build-arch: x64 + slack-name: ${{ github.event.pull_request.head.ref }} + secrets: inherit diff --git a/.gitignore b/.gitignore index 28d589b..cde1485 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode/ dist dist-* cabal-dev diff --git a/app-http-insecure/Main.hs b/app-http-insecure/Main.hs index 887082b..81341f5 100644 --- a/app-http-insecure/Main.hs +++ b/app-http-insecure/Main.hs @@ -1,35 +1,41 @@ -{-# language OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings #-} -import Control.Exception (bracket,throwIO) +import Control.Exception (bracket, throwIO) import Http.Exchange.Network (exchange) -import Http.Types (Request(..),RequestLine(..),Bodied(..),Header(Header)) +import Http.Headers qualified as Headers +import Http.Types (Bodied (..), Header (Header), Request (..), RequestLine (..)) +import Network.Socket qualified as N import Text.Show.Pretty (pPrint) -import qualified Http.Headers as Headers -import qualified Network.Socket as N main :: IO () main = do - let hints = N.defaultHints { N.addrSocketType = N.Stream } + let hints = N.defaultHints {N.addrSocketType = N.Stream} minfo <- N.getAddrInfo (Just hints) (Just "ifconfig.me") (Just "80") info <- case minfo of info : _ -> pure info [] -> fail "Impossible: getAddrInfo cannot return empty list" bracket (N.openSocket info) N.close $ \sock -> do N.connect sock (N.addrAddress info) - result <- exchange sock Bodied - { metadata = Request - { requestLine = RequestLine - { method = "GET" - , path = "/ip" + result <- + exchange + sock + Bodied + { metadata = + Request + { requestLine = + RequestLine + { method = "GET" + , path = "/ip" + } + , headers = + Headers.fromList + [ Header "Host" "ifconfig.me" + , Header "Accept" "text/plain" + , Header "User-Agent" "curl/0.0.0" + ] + } + , body = mempty } - , headers = Headers.fromList - [ Header "Host" "ifconfig.me" - , Header "Accept" "text/plain" - , Header "User-Agent" "curl/0.0.0" - ] - } - , body = mempty - } case result of Left e -> throwIO e Right resp -> pPrint resp diff --git a/app-http-secure/Main.hs b/app-http-secure/Main.hs index 0ec14ce..a3b81b4 100644 --- a/app-http-secure/Main.hs +++ b/app-http-secure/Main.hs @@ -1,36 +1,40 @@ -{-# language OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings #-} -import Control.Exception (bracket,throwIO) -import Http.Exchange.Tls (SocketThrowingNetworkException(..),exchange) -import Http.Types (Request(..),RequestLine(..),Bodied(..),Header(Header)) -import Text.Show.Pretty (pPrint) +import Control.Exception (bracket, throwIO) import Data.Default (def) -import qualified Http.Headers as Headers -import qualified Network.Socket as N -import qualified Network.TLS.Extra.Cipher as Tls -import qualified Network.TLS as Tls +import Http.Exchange.Tls (SocketThrowingNetworkException (..), exchange) +import Http.Headers qualified as Headers +import Http.Types (Bodied (..), Header (Header), Request (..), RequestLine (..)) +import Network.Socket qualified as N +import Network.TLS qualified as Tls +import Network.TLS.Extra.Cipher qualified as Tls +import Text.Show.Pretty (pPrint) main :: IO () main = do - let noValidation = Tls.ValidationCache - (\_ _ _ -> return Tls.ValidationCachePass) - (\_ _ _ -> return ()) - let clientParams = (Tls.defaultParamsClient "ifconfig.me" mempty) - { Tls.clientSupported = def - { Tls.supportedVersions = [Tls.TLS13] - , Tls.supportedCiphers = - [ Tls.cipher_TLS13_AES128GCM_SHA256 - , Tls.cipher_TLS13_AES256GCM_SHA384 - , Tls.cipher_TLS13_CHACHA20POLY1305_SHA256 - , Tls.cipher_TLS13_AES128CCM_SHA256 - , Tls.cipher_TLS13_AES128CCM8_SHA256 - ] + let noValidation = + Tls.ValidationCache + (\_ _ _ -> return Tls.ValidationCachePass) + (\_ _ _ -> return ()) + let clientParams = + (Tls.defaultParamsClient "ifconfig.me" mempty) + { Tls.clientSupported = + def + { Tls.supportedVersions = [Tls.TLS13] + , Tls.supportedCiphers = + [ Tls.cipher_TLS13_AES128GCM_SHA256 + , Tls.cipher_TLS13_AES256GCM_SHA384 + , Tls.cipher_TLS13_CHACHA20POLY1305_SHA256 + , Tls.cipher_TLS13_AES128CCM_SHA256 + , Tls.cipher_TLS13_AES128CCM8_SHA256 + ] + } + , Tls.clientShared = + def + { Tls.sharedValidationCache = noValidation + } } - , Tls.clientShared = def - { Tls.sharedValidationCache = noValidation - } - } - let hints = N.defaultHints { N.addrSocketType = N.Stream } + let hints = N.defaultHints {N.addrSocketType = N.Stream} minfo <- N.getAddrInfo (Just hints) (Just "ifconfig.me") (Just "443") info <- case minfo of info : _ -> pure info @@ -39,21 +43,26 @@ main = do N.connect sock (N.addrAddress info) ctx <- Tls.contextNew (SocketThrowingNetworkException sock) clientParams Tls.handshake ctx - result <- exchange ctx Bodied - { metadata = Request - { requestLine = RequestLine - { method = "GET" - , path = "/ip" + result <- + exchange + ctx + Bodied + { metadata = + Request + { requestLine = + RequestLine + { method = "GET" + , path = "/ip" + } + , headers = + Headers.fromList + [ Header "Host" "ifconfig.me" + , Header "Accept" "text/plain" + , Header "User-Agent" "curl/0.0.0" + ] + } + , body = mempty } - , headers = Headers.fromList - [ Header "Host" "ifconfig.me" - , Header "Accept" "text/plain" - , Header "User-Agent" "curl/0.0.0" - ] - } - , body = mempty - } case result of Left e -> throwIO e Right resp -> pPrint resp - diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..40cd005 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,51 @@ +# Number of spaces per indentation step +indentation: 2 + +# Max line length for automatic line breaking +column-limit: 200 + +# Styling of arrows in type signatures (choices: trailing, leading, or leading-args) +function-arrows: trailing + +# How to place commas in multi-line lists, records, etc. (choices: leading or trailing) +comma-style: leading + +# Styling of import/export lists (choices: leading, trailing, or diff-friendly) +import-export-style: leading + +# Whether to full-indent or half-indent 'where' bindings past the preceding body +indent-wheres: false + +# Whether to leave a space before an opening record brace +record-brace-space: true + +# Number of spaces between top-level declarations +newlines-between-decls: 1 + +# How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact) +haddock-style: multi-line + +# How to print module docstring +haddock-style-module: null + +# Styling of let blocks (choices: auto, inline, newline, or mixed) +let-style: auto + +# How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space) +in-style: right-align + +# Whether to put parentheses around a single constraint (choices: auto, always, or never) +single-constraint-parens: always + +# Output Unicode syntax (choices: detect, always, or never) +unicode: never + +# Give the programmer more choice on where to insert blank lines +respectful: true + +# Fixity information for operators +fixities: [] + +# Module reexports Fourmolu should know about +reexports: [] + diff --git a/http-exchange-instantiations.cabal b/http-exchange-instantiations.cabal index d9af63e..5fb7873 100644 --- a/http-exchange-instantiations.cabal +++ b/http-exchange-instantiations.cabal @@ -1,83 +1,87 @@ -cabal-version: 3.0 -name: http-exchange-instantiations -version: 0.1.3.0 -synopsis: Instantiations of http-exchange +cabal-version: 3.0 +name: http-exchange-instantiations +version: 0.1.3.0 +synopsis: Instantiations of http-exchange + -- description: -license: BSD-3-Clause -license-file: LICENSE -author: Andrew Martin -maintainer: andrew.thaddeus@gmail.com -copyright: 2023 Andrew Martin -category: Network -build-type: Simple +license: BSD-3-Clause +license-file: LICENSE +author: Andrew Martin +maintainer: andrew.thaddeus@gmail.com +copyright: 2023 Andrew Martin +category: Network +build-type: Simple extra-doc-files: CHANGELOG.md library chanimpl - ghc-options: -Wall + ghc-options: -Wall exposed-modules: SocketChannel SocketInterruptibleChannel TlsChannel - -- TlsInterruptibleChannel + + -- TlsInterruptibleChannel build-depends: - , base >=4.16.3.0 && <5 - , network-unexceptional >=0.2 - , network >=3.1.4 - , tls >=1.8 - , error-codes >=0.1.1 - , bytestring >=0.11 - , byteslice >=0.2.11 - , stm >=2.5.1.0 - hs-source-dirs: src-chanimpl + , base >=4.16.3.0 && <5 + , byteslice >=0.2.11 + , bytestring >=0.11 + , error-codes >=0.1.1 + , network >=3.1.4 + , network-unexceptional >=0.2 + , stm >=2.5.1.0 + , tls >=1.8 + + hs-source-dirs: src-chanimpl default-language: GHC2021 library - ghc-options: -Wall + ghc-options: -Wall exposed-modules: Http.Exchange.Network Http.Exchange.Tls + build-depends: - , base >=4.16.3.0 + , base >=4.16.3.0 + , bytestring >=0.11 , chanimpl - , http-exchange >=0.1.1 - , http-interchange >=0.3.1 - , network >=3.1.4 - , stm >=2.5.1.0 - , tls >=1.7 - , network-unexceptional >=0.2 - , bytestring >=0.11 - hs-source-dirs: src + , http-exchange >=0.1.1 + , http-interchange >=0.3.1 + , network >=3.1.4 + , network-unexceptional >=0.2 + , stm >=2.5.1.0 + , tls >=1.7 + + hs-source-dirs: src default-language: GHC2021 mixins: - http-exchange (Exchange as SocketExchange) - requires (Channel as SocketChannel), - http-exchange (Exchange as TlsExchange) - requires (Channel as TlsChannel), - http-exchange (Exchange as SocketInterruptibleExchange) - requires (Channel as SocketInterruptibleChannel) + http-exchange (Exchange as SocketExchange) requires (Channel as SocketChannel), + http-exchange (Exchange as TlsExchange) requires (Channel as TlsChannel), + http-exchange (Exchange as SocketInterruptibleExchange) requires (Channel as SocketInterruptibleChannel) executable http-insecure - ghc-options: -Wall - main-is: Main.hs + ghc-options: -Wall + main-is: Main.hs build-depends: - , base >=4.16.3.0 - , network >=3.1.4 - , http-interchange >=0.3.1 + , base >=4.16.3.0 , http-exchange-instantiations - , pretty-show >=1.10 - hs-source-dirs: app-http-insecure + , http-interchange >=0.3.1 + , network >=3.1.4 + , pretty-show >=1.10 + + hs-source-dirs: app-http-insecure default-language: GHC2021 executable http-secure - ghc-options: -Wall - main-is: Main.hs + ghc-options: -Wall + main-is: Main.hs build-depends: - , base >=4.16.3.0 - , network >=3.1.4 - , http-interchange >=0.3.1 + , base >=4.16.3.0 + , data-default >=0.7.1 , http-exchange-instantiations - , pretty-show >=1.10 - , tls >=1.7 - , data-default >=0.7.1 - hs-source-dirs: app-http-secure + , http-interchange >=0.3.1 + , network >=3.1.4 + , pretty-show >=1.10 + , tls >=1.7 + + hs-source-dirs: app-http-secure default-language: GHC2021 diff --git a/src-chanimpl/SocketChannel.hs b/src-chanimpl/SocketChannel.hs index 6a7c2d4..4b0dbcb 100644 --- a/src-chanimpl/SocketChannel.hs +++ b/src-chanimpl/SocketChannel.hs @@ -11,12 +11,12 @@ module SocketChannel import Data.Bytes (Bytes) import Data.Bytes.Chunks (Chunks) -import Network.Socket (Socket) import Foreign.C.Error (Errno) +import Network.Socket (Socket) -import qualified Foreign.C.Error.Describe as Describe -import qualified Network.Unexceptional.Bytes as NB -import qualified Network.Unexceptional.Chunks as NC +import Foreign.C.Error.Describe qualified as Describe +import Network.Unexceptional.Bytes qualified as NB +import Network.Unexceptional.Chunks qualified as NC type M = IO @@ -34,12 +34,12 @@ showsPrecErrno :: Int -> Errno -> String -> String showsPrecErrno _ e s = Describe.string e ++ (' ' : s) send :: - Resource - -> Chunks - -> M (Either Errno ()) -send a b = NC.send a b + Resource -> + Chunks -> + M (Either Errno ()) +send = NC.send receive :: - Resource - -> M (Either Errno Bytes) + Resource -> + M (Either Errno Bytes) receive a = NB.receive a 12000 diff --git a/src-chanimpl/SocketInterruptibleChannel.hs b/src-chanimpl/SocketInterruptibleChannel.hs index a54557d..4ada72a 100644 --- a/src-chanimpl/SocketInterruptibleChannel.hs +++ b/src-chanimpl/SocketInterruptibleChannel.hs @@ -4,20 +4,20 @@ module SocketInterruptibleChannel , ReceiveException , showsPrecSendException , showsPrecReceiveException - , Resource(..) + , Resource (..) , send , receive ) where +import Control.Concurrent.STM (TVar) import Data.Bytes (Bytes) import Data.Bytes.Chunks (Chunks) -import Network.Socket (Socket) import Foreign.C.Error (Errno) -import Control.Concurrent.STM (TVar) +import Network.Socket (Socket) -import qualified Foreign.C.Error.Describe as Describe -import qualified Network.Unexceptional.Bytes as NB -import qualified Network.Unexceptional.Chunks as NC +import Foreign.C.Error.Describe qualified as Describe +import Network.Unexceptional.Bytes qualified as NB +import Network.Unexceptional.Chunks qualified as NC type M = IO @@ -36,14 +36,13 @@ showsPrecErrno :: Int -> Errno -> String -> String showsPrecErrno _ e s = Describe.string e ++ (' ' : s) send :: - Resource - -> Chunks - -> M (Either Errno ()) + Resource -> + Chunks -> + M (Either Errno ()) send (Resource a interrupt) b = do NC.sendInterruptible interrupt a b receive :: - Resource - -> M (Either Errno Bytes) + Resource -> + M (Either Errno Bytes) receive (Resource a interrupt) = NB.receiveInterruptible interrupt a 12000 - diff --git a/src-chanimpl/TlsChannel.hs b/src-chanimpl/TlsChannel.hs index 804525f..3d2fd3a 100644 --- a/src-chanimpl/TlsChannel.hs +++ b/src-chanimpl/TlsChannel.hs @@ -1,37 +1,31 @@ -{-# language DerivingStrategies #-} -{-# language LambdaCase #-} -{-# language DeriveAnyClass #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE LambdaCase #-} module TlsChannel ( M - , TransportException(..) + , TransportException (..) , SendException , ReceiveException , showsPrecSendException , showsPrecReceiveException , Resource - , NetworkException(..) + , NetworkException (..) , send , receive , tryTls ) where +import Control.Exception (Exception, IOException, try) import Data.Bytes (Bytes) -import Data.ByteString (ByteString) import Data.Bytes.Chunks (Chunks) -import Control.Exception (Exception,IOException,try,throwIO) -import Network.Socket (Socket) import Foreign.C.Error (Errno) -import qualified Data.Bytes as Bytes -import qualified Data.List as List -import qualified Data.Bytes.Chunks as Chunks -import qualified Network.Socket as N -import qualified Network.Unexceptional.ByteString as NBS -import qualified Network.TLS as Tls -import qualified Data.ByteString.Lazy as LBS -import qualified Data.ByteString as ByteString -import qualified Foreign.C.Error.Describe as Describe +import Data.ByteString.Lazy qualified as LBS +import Data.Bytes qualified as Bytes +import Data.Bytes.Chunks qualified as Chunks +import Foreign.C.Error.Describe qualified as Describe +import Network.TLS qualified as Tls type M = IO @@ -53,12 +47,18 @@ showsPrecReceiveException :: Int -> ReceiveException -> String -> String showsPrecReceiveException = showsPrec instance Show TransportException where - showsPrec d (Network e) = showParen (d > 10) - (showString "Network " . showsPrecErrno 11 e) - showsPrec d (System e) = showParen (d > 10) - (showString "System " . showsPrec 11 e) - showsPrec d (TlsException e) = showParen (d > 10) - (showString "TlsException " . showsPrec 11 e) + showsPrec d (Network e) = + showParen + (d > 10) + (showString "Network " . showsPrecErrno 11 e) + showsPrec d (System e) = + showParen + (d > 10) + (showString "System " . showsPrec 11 e) + showsPrec d (TlsException e) = + showParen + (d > 10) + (showString "TlsException " . showsPrec 11 e) data NetworkException = NetworkException !Errno deriving anyclass (Exception) @@ -66,22 +66,24 @@ data NetworkException = NetworkException !Errno instance Show NetworkException where show (NetworkException e) = Describe.string e --- | There are three types of exceptions that we can get when --- sending/receiving data, so we nest the call to sendData in three --- try statements to catch all the possible exceptions. +{- | There are three types of exceptions that we can get when +sending/receiving data, so we nest the call to sendData in three +try statements to catch all the possible exceptions. +-} send :: - Tls.Context - -> Chunks - -> IO (Either TransportException ()) + Tls.Context -> + Chunks -> + IO (Either TransportException ()) send ctx ch = tryTls $ Tls.sendData ctx (LBS.fromStrict (Chunks.concatByteString ch)) receive :: - Tls.Context - -> M (Either TransportException Bytes) -receive a = tryTls (Tls.recvData a) >>= \case - Left err -> pure (Left err) - Right b -> pure $! Right $! Bytes.fromByteString b + Tls.Context -> + M (Either TransportException Bytes) +receive a = + tryTls (Tls.recvData a) >>= \case + Left err -> pure (Left err) + Right b -> pure $! Right $! Bytes.fromByteString b tryTls :: IO a -> IO (Either TransportException a) tryTls action = do diff --git a/src/Http/Exchange/Network.hs b/src/Http/Exchange/Network.hs index d7ab55c..7e66ec1 100644 --- a/src/Http/Exchange/Network.hs +++ b/src/Http/Exchange/Network.hs @@ -1,60 +1,75 @@ -{-# language LambdaCase #-} +{-# LANGUAGE LambdaCase #-} --- | Issue insecure HTTP requests using the 'Socket' type from the @network@ --- library. +{- | Issue insecure HTTP requests using the 'Socket' type from the @network@ +library. +-} module Http.Exchange.Network ( -- * Issue Requests exchange , exchangeInterruptible , exchangeTimeout + -- * Example Use -- $example - -- * Exceptions -- $exceptionnotes - , Exception(..) - , HttpException(..) + , Exception (..) + , HttpException (..) ) where -import Network.Socket (Socket) -import Http.Types (Request,Bodied,Response) -import SocketExchange (Exception(..),HttpException(..)) -import Control.Concurrent.STM (TVar,registerDelay) +import Control.Concurrent.STM (TVar, registerDelay) import Data.Bifunctor (first) +import Http.Types (Bodied, Request, Response) +import Network.Socket (Socket) +import SocketExchange (Exception (..), HttpException (..)) -import qualified SocketInterruptibleChannel as YChan -import qualified SocketInterruptibleExchange as Y -import qualified SocketExchange as X +import SocketExchange qualified as X +import SocketInterruptibleChannel qualified as YChan +import SocketInterruptibleExchange qualified as Y --- | Issue an HTTP request and await a response. This is does not use TLS --- (i.e. HTTP, not HTTPS). This function returns exceptions in @Left@ rather --- than throwing them, so it is not necessary to use @catch@ when calling it. +{- | Issue an HTTP request and await a response. This is does not use TLS +(i.e. HTTP, not HTTPS). This function returns exceptions in @Left@ rather +than throwing them, so it is not necessary to use @catch@ when calling it. +-} exchange :: - Socket -- ^ Network socket (TCP or Unix-Domain) - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception + -- | Network socket (TCP or Unix-Domain) + Socket -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchange = X.exchange --- | Variant of exchange that abandons the attempt if the interrupt --- variable is set to @True@. If the operation is interrupted in this --- way, the result is @EAGAIN@ wrapped by either @Send@ or @Receive@. --- See the implementation of 'exchangeTimeout' for an example of how to --- use this function to timeout if the HTTP exchange does not complete --- quickly. -exchangeInterruptible :: - TVar Bool -- ^ Interrupt - -> Socket -- ^ Network socket (TCP or Unix-Domain) - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception +{- | Variant of exchange that abandons the attempt if the interrupt +variable is set to @True@. If the operation is interrupted in this +way, the result is @EAGAIN@ wrapped by either @Send@ or @Receive@. +See the implementation of 'exchangeTimeout' for an example of how to +use this function to timeout if the HTTP exchange does not complete +quickly. +-} +exchangeInterruptible :: + -- | Interrupt + TVar Bool -> + -- | Network socket (TCP or Unix-Domain) + Socket -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchangeInterruptible !a b c = - (fmap (first convertException) (Y.exchange (YChan.Resource b a) c)) - --- | Variant of 'exchange' that abandons the exchange if it has not --- completed in a given number of microseconds. -exchangeTimeout :: - Int -- ^ Microseconds to wait before giving up - -> Socket -- ^ Network socket (TCP or Unix-Domain) - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception + fmap (first convertException) (Y.exchange (YChan.Resource b a) c) + +{- | Variant of 'exchange' that abandons the exchange if it has not +completed in a given number of microseconds. +-} +exchangeTimeout :: + -- | Microseconds to wait before giving up + Int -> + -- | Network socket (TCP or Unix-Domain) + Socket -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchangeTimeout !t sock req = do interrupt <- registerDelay t exchangeInterruptible interrupt sock req @@ -136,7 +151,6 @@ Running this results in this being printed: > } > , body = ... > } - -} {- $exceptionnotes diff --git a/src/Http/Exchange/Tls.hs b/src/Http/Exchange/Tls.hs index eb68fed..2acb4ea 100644 --- a/src/Http/Exchange/Tls.hs +++ b/src/Http/Exchange/Tls.hs @@ -1,106 +1,119 @@ -{-# language LambdaCase #-} +{-# LANGUAGE LambdaCase #-} --- | Issue HTTPS requests using the 'Context' type from the @tls@ --- library. +{- | Issue HTTPS requests using the 'Context' type from the @tls@ +library. +-} module Http.Exchange.Tls ( -- * Issue Requests exchange - -- * Issue Interruptible Requests + + -- * Issue Interruptible Requests , exchangeInterruptible , exchangeTimeout , interruptibleContextNew , interruptibleHandshake , exposeInterruptibleContext + -- * Types , InterruptibleContext - , SocketThrowingNetworkException(..) - , NetworkException(..) - , TransportException(..) + , SocketThrowingNetworkException (..) + , NetworkException (..) + , TransportException (..) + -- * Example Use -- $example - -- * Exceptions -- $exceptionnotes - , Exception(..) - , HttpException(..) + , Exception (..) + , HttpException (..) ) where +import Http.Types (Bodied, Request, Response) import Network.TLS (Context) -import Http.Types (Request,Bodied,Response) -import TlsChannel (NetworkException(..),tryTls) -import TlsChannel (TransportException(..)) -import Control.Exception (IOException,try,throwIO) -import TlsExchange (Exception(..),HttpException(..)) +import Control.Concurrent.STM (TVar, registerDelay) +import Control.Exception (throwIO) +import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Network.Socket (Socket) -import Foreign.C.Error (Errno) -import Foreign.C.Error (Errno) -import Data.IORef (IORef,readIORef,writeIORef,newIORef) -import Control.Concurrent.STM (TVar,registerDelay) -import Data.ByteString (ByteString) -import qualified TlsExchange as X -import qualified Data.List as List -import qualified Data.ByteString as ByteString -import qualified Network.Socket as N -import qualified Network.Unexceptional.ByteString as NBS -import qualified Network.TLS as Tls -import qualified Network.Unexceptional.Chunks as NC +import Network.Socket qualified as N +import Network.TLS qualified as Tls +import Network.Unexceptional.ByteString qualified as NBS +import TlsChannel (NetworkException (..), TransportException (..), tryTls) +import TlsExchange (Exception (..), HttpException (..)) +import TlsExchange qualified as X --- | Issue an HTTP request and await a response. This is does not use TLS --- (i.e. HTTP, not HTTPS). This function returns exceptions in @Left@ rather --- than throwing them, so it is not necessary to use @catch@ when calling it. +{- | Issue an HTTP request and await a response. This is does not use TLS +(i.e. HTTP, not HTTPS). This function returns exceptions in @Left@ rather +than throwing them, so it is not necessary to use @catch@ when calling it. +-} exchange :: - Context -- ^ TLS Context - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception + -- | TLS Context + Context -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchange = X.exchange --- | Variant of exchange that abandons the attempt if the interrupt --- variable is set to @True@. The design of the @tls@ library complicates --- this function's signature and use. There is an 'InterruptibleContext' --- type defined in this module that must be used with this function. --- It is not possible to use the ordinary @Context@ type from @tls@. --- Example use: --- --- > clientParams <- ... -- elided for brevity --- > theAddressInfo <- ... -- elided for brevity --- > sock <- ... -- elided for brevity --- > N.connect sock theAddressInfo --- > ctx <- interruptibleContextNew sock clientParams --- > Tls.handshake ctx --- > interrupt <- registerDelay 1_000_000 --- > result <- exchange interrupt ctx Bodied{..} -- request body elided +{- | Variant of exchange that abandons the attempt if the interrupt +variable is set to @True@. The design of the @tls@ library complicates +this function's signature and use. There is an 'InterruptibleContext' +type defined in this module that must be used with this function. +It is not possible to use the ordinary @Context@ type from @tls@. +Example use: + +> clientParams <- ... -- elided for brevity +> theAddressInfo <- ... -- elided for brevity +> sock <- ... -- elided for brevity +> N.connect sock theAddressInfo +> ctx <- interruptibleContextNew sock clientParams +> Tls.handshake ctx +> interrupt <- registerDelay 1_000_000 +> result <- exchange interrupt ctx Bodied{..} -- request body elided +-} exchangeInterruptible :: - TVar Bool -- ^ Interrupt - -> InterruptibleContext -- ^ TLS Context supporting interruption - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception + -- | Interrupt + TVar Bool -> + -- | TLS Context supporting interruption + InterruptibleContext -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchangeInterruptible !intr (InterruptibleContext ctx intrRef) !req = do writeIORef intrRef intr r <- X.exchange ctx req writeIORef intrRef interruptibleContextError pure r --- | TLS handshake that can be interrupted. Unlike the original handshake --- from the @tls@ library, this returns exceptions rather than throwing them. --- This function must be called before performing any HTTP exchanges on --- the interruptible context. +{- | TLS handshake that can be interrupted. Unlike the original handshake +from the @tls@ library, this returns exceptions rather than throwing them. +This function must be called before performing any HTTP exchanges on +the interruptible context. +-} interruptibleHandshake :: - TVar Bool -- ^ Interrupt - -> InterruptibleContext -- ^ TLS Context supporting interruption - -> IO (Either TransportException ()) + -- | Interrupt + TVar Bool -> + -- | TLS Context supporting interruption + InterruptibleContext -> + IO (Either TransportException ()) interruptibleHandshake !intr (InterruptibleContext ctx intrRef) = do writeIORef intrRef intr x <- tryTls (Tls.handshake ctx) writeIORef intrRef interruptibleContextError pure x --- | Variant of 'exchange' that abandons the exchange if it has not --- completed in a given number of microseconds. -exchangeTimeout :: - Int -- ^ Microseconds to wait before giving up - -> InterruptibleContext -- ^ TLS Context supporting interruption - -> Bodied Request -- ^ HTTP Request - -> IO (Either Exception (Bodied Response)) -- ^ HTTP Response or exception +{- | Variant of 'exchange' that abandons the exchange if it has not +completed in a given number of microseconds. +-} +exchangeTimeout :: + -- | Microseconds to wait before giving up + Int -> + -- | TLS Context supporting interruption + InterruptibleContext -> + -- | HTTP Request + Bodied Request -> + -- | HTTP Response or exception + IO (Either Exception (Bodied Response)) exchangeTimeout !t ctx req = do interrupt <- registerDelay t exchangeInterruptible interrupt ctx req @@ -195,8 +208,6 @@ Running this results in this being printed: > } > , body = ... > } - - -} {- $exceptionnotes @@ -207,10 +218,11 @@ module, but in this instantiation, these types are both aliases for 'Foreign.C.Error.Errno'. -} --- | Wraps the Socket type. This has different HasBackend instance that --- throws NetworkException instead of IOException. --- Elsewhere, when we call Tls.contextNew to create a TLS context, --- we must use this type instead of Socket. +{- | Wraps the Socket type. This has different HasBackend instance that +throws NetworkException instead of IOException. +Elsewhere, when we call Tls.contextNew to create a TLS context, +we must use this type instead of Socket. +-} newtype SocketThrowingNetworkException = SocketThrowingNetworkException Socket @@ -218,62 +230,71 @@ data InterruptibleContext = InterruptibleContext !Tls.Context !(IORef (TVar Bool)) interruptibleContextError :: TVar Bool -{-# noinline interruptibleContextError #-} +{-# NOINLINE interruptibleContextError #-} interruptibleContextError = errorWithoutStackTrace "Http.Exchange.Tls: misuse of InterruptibleContext" --- | Create a new TLS context that supports interrupting exchanges --- with a 'TVar'. -interruptibleContextNew :: (Tls.TLSParams params) - => Socket -- ^ Network socket. Must already be connected. - -> params -- ^ Parameters of the context. - -> IO InterruptibleContext +{- | Create a new TLS context that supports interrupting exchanges +with a 'TVar'. +-} +interruptibleContextNew :: + (Tls.TLSParams params) => + -- | Network socket. Must already be connected. + Socket -> + -- | Parameters of the context. + params -> + IO InterruptibleContext interruptibleContextNew socket params = do !intrRef <- newIORef interruptibleContextError let backend = buildInterruptibleBackend socket intrRef context <- Tls.contextNew backend params pure (InterruptibleContext context intrRef) --- | Expose the TLS context. Do not call TLS data-exchange functions like --- @sendData@, @recvData@, or @handshake@ on this context. This context is --- exposed so that the caller can query it for metadata about the session --- (certs, etc.). +{- | Expose the TLS context. Do not call TLS data-exchange functions like +@sendData@, @recvData@, or @handshake@ on this context. This context is +exposed so that the caller can query it for metadata about the session +(certs, etc.). +-} exposeInterruptibleContext :: InterruptibleContext -> Tls.Context -{-# inline exposeInterruptibleContext #-} +{-# INLINE exposeInterruptibleContext #-} exposeInterruptibleContext (InterruptibleContext c _) = c buildInterruptibleBackend :: Socket -> IORef (TVar Bool) -> Tls.Backend -buildInterruptibleBackend s !intrRef = Tls.Backend - { Tls.backendFlush = pure () - , Tls.backendClose = N.close s - , Tls.backendSend = \b -> do - !interrupt <- readIORef intrRef - NBS.sendInterruptible interrupt s b >>= \case - Left e -> throwIO (NetworkException e) - Right () -> pure () - , Tls.backendRecv = \n -> do - !interrupt <- readIORef intrRef - NBS.receiveExactlyInterruptible interrupt s n >>= \case - Left e -> throwIO (NetworkException e) - Right bs -> pure bs - } +buildInterruptibleBackend s !intrRef = + Tls.Backend + { Tls.backendFlush = pure () + , Tls.backendClose = N.close s + , Tls.backendSend = \b -> do + !interrupt <- readIORef intrRef + NBS.sendInterruptible interrupt s b >>= \case + Left e -> throwIO (NetworkException e) + Right () -> pure () + , Tls.backendRecv = \n -> do + !interrupt <- readIORef intrRef + NBS.receiveExactlyInterruptible interrupt s n >>= \case + Left e -> throwIO (NetworkException e) + Right bs -> pure bs + } instance Tls.HasBackend SocketThrowingNetworkException where initializeBackend _ = pure () getBackend (SocketThrowingNetworkException s) = buildBackendThrowingNetworkException s buildBackendThrowingNetworkException :: Socket -> Tls.Backend -buildBackendThrowingNetworkException !s = Tls.Backend - { Tls.backendFlush = pure () - , Tls.backendClose = N.close s - , Tls.backendSend = \b -> NBS.send s b >>= \case - Left e -> throwIO (NetworkException e) - Right () -> pure () - -- Note: This receive function does not imitate the behavior of the - -- auxiliary function recvAll defined in Network.TLS.Backend. If the - -- peer performs an orderly shutdown without sending enough bytes, - -- this throws EEOI. - , Tls.backendRecv = \n -> NBS.receiveExactly s n >>= \case - Left e -> throwIO (NetworkException e) - Right bs -> pure bs - } +buildBackendThrowingNetworkException !s = + Tls.Backend + { Tls.backendFlush = pure () + , Tls.backendClose = N.close s + , Tls.backendSend = \b -> + NBS.send s b >>= \case + Left e -> throwIO (NetworkException e) + Right () -> pure () + , -- Note: This receive function does not imitate the behavior of the + -- auxiliary function recvAll defined in Network.TLS.Backend. If the + -- peer performs an orderly shutdown without sending enough bytes, + -- this throws EEOI. + Tls.backendRecv = \n -> + NBS.receiveExactly s n >>= \case + Left e -> throwIO (NetworkException e) + Right bs -> pure bs + }