packages feed

wai-handler-hal (empty) → 0.1.0.0

raw patch · 5 files changed

+390/−0 lines, 5 filesdep +basedep +base64-bytestringdep +bytestringsetup-changed

Dependencies added: base, base64-bytestring, bytestring, case-insensitive, hal, http-types, network, text, unordered-containers, vault, wai

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for wai-handler-hal++## 0.1.0.0 -- 2021-04-15++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (C) 2021 Bellroy Pty Ltd++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the+   distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived+   from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Network/Wai/Handler/Hal.hs view
@@ -0,0 +1,295 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TupleSections #-}+{-# OPTIONS_GHC -Wno-deprecations #-}++-- |+--+-- Module      : Network.Wai.Handler.Hal+-- Copyright   : (C) 2021 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+-- Stability   : experimental+--+-- Lifts an 'Wai.Application' so that it can be run using+-- 'AWS.Lambda.Runtime.mRuntime' or+-- 'AWS.Lambda.Runtime.mRuntimeWithContext''. The glue code will look+-- something like this:+--+-- @+-- import AWS.Lambda.Runtime ('AWS.Lambda.Runtime.mRuntime')+-- import Network.Wai (Application)+-- import qualified Network.Wai.Handler.Hal as WaiHandler+--+-- app :: Application+-- app = undefined -- From Servant or wherever else+--+-- main :: IO ()+-- main = 'AWS.Lambda.Runtime.mRuntime' $ WaiHandler.'run' app+-- @+module Network.Wai.Handler.Hal+  ( run,+    runWithContext,+    toWaiRequest,+    fromWaiResponse,+  )+where++import AWS.Lambda.Context (LambdaContext)+import qualified AWS.Lambda.Events.ApiGateway.ProxyRequest as HalRequest+  ( RequestContext (identity),+  )+import qualified AWS.Lambda.Events.ApiGateway.ProxyRequest as HalRequest hiding+  ( RequestContext (..),+  )+import qualified AWS.Lambda.Events.ApiGateway.ProxyResponse as HalResponse+import Control.Monad.IO.Class (MonadIO (..))+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Builder.Extra as Builder+import qualified Data.ByteString.Lazy as BL+import qualified Data.CaseInsensitive as CI+import Data.Function ((&))+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as H+import qualified Data.IORef as IORef+import Data.List (foldl')+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Vault.Lazy (Key, Vault)+import qualified Data.Vault.Lazy as Vault+import Network.HTTP.Types.Header+  ( HeaderName,+    ResponseHeaders,+    hContentType,+    hHost,+    hRange,+    hReferer,+    hUserAgent,+  )+import Network.HTTP.Types.URI+  ( Query,+    QueryItem,+    encodePath,+    queryTextToQuery,+    renderQuery,+  )+import Network.HTTP.Types.Version (HttpVersion (..))+import Network.Socket (PortNumber)+import qualified Network.Socket as NS+import qualified Network.Wai as Wai+import qualified Network.Wai.Internal as Wai+import System.IO (IOMode (..), SeekMode (..), hSeek, withFile)++-- | Convert a WAI 'Wai.Application' into a function that can+-- be run by hal's 'AWS.Lambda.Runtime.mRuntime'. This is the simplest+-- form, and probably all that you'll need. See 'runWithContext' if+-- you have more complex needs.+run ::+  MonadIO m =>+  Wai.Application ->+  HalRequest.ProxyRequest HalRequest.NoAuthorizer ->+  m HalResponse.ProxyResponse+run app req = liftIO $ do+  waiReq <- toWaiRequest Vault.empty 443 req+  responseRef <- IORef.newIORef Nothing+  Wai.ResponseReceived <- app waiReq $ \waiResp ->+    Wai.ResponseReceived <$ IORef.writeIORef responseRef (Just waiResp)+  Just waiResp <- IORef.readIORef responseRef+  fromWaiResponse waiResp++-- | Convert a WAI 'Wai.Application' into a function that can+-- be run by hal's 'AWS.Lambda.Runtime.mRuntimeWithContext''. This+-- function exposes all the configurable knobs.+runWithContext ::+  MonadIO m =>+  -- | Vault of values to share between the application and any+  -- middleware. You can pass in @Data.Vault.Lazy.'Vault.empty'@, or+  -- 'mempty' if you don't want to depend on @vault@ directly.+  Vault ->+  -- | API Gateway doesn't tell us the port it's listening on, so you+  -- have to tell it yourself. This is almost always going to be 443+  -- (HTTPS).+  PortNumber ->+  -- | We pass two 'Vault' keys to the callback that provides the+  -- 'Wai.Application'. This allows the application to look into the+  -- 'Vault' part of each request and read @hal@ data structures, if+  -- necessary:+  --+  -- * The @'Key' 'LambdaContext'@ provides+  --   information about the Lambda invocation, function, and+  --   execution environment; and+  --+  -- * The @'Key' ('HalRequest.ProxyRequest'+  -- 'HalRequest.NoAuthorizer')@ provides the unmodified API Gateway+  -- representation of the HTTP request.+  ( Key LambdaContext ->+    Key (HalRequest.ProxyRequest HalRequest.NoAuthorizer) ->+    Wai.Application+  ) ->+  LambdaContext ->+  -- | We force 'HalRequest.NoAuthorizer' because it's a type alias+  -- for 'Data.Aeson.Value' (i.e., should always parse), and it avoids+  -- an "ambiguous type variable" error at the use site.+  HalRequest.ProxyRequest HalRequest.NoAuthorizer ->+  m HalResponse.ProxyResponse+runWithContext vault port app ctx req = liftIO $ do+  contextKey <- Vault.newKey+  requestKey <- Vault.newKey+  let vault' =+        vault+          & Vault.insert contextKey ctx+          & Vault.insert requestKey req+  waiReq <- toWaiRequest vault' port req+  responseRef <- IORef.newIORef Nothing+  Wai.ResponseReceived <- app contextKey requestKey waiReq $ \waiResp ->+    Wai.ResponseReceived <$ IORef.writeIORef responseRef (Just waiResp)+  Just waiResp <- IORef.readIORef responseRef+  fromWaiResponse waiResp++-- | Convert the request sent to a Lambda serving an API Gateway proxy+-- integration into a WAI request.+--+-- __Note:__ We aren't told the HTTP version the client is using, so+-- we assume HTTP 1.1.+toWaiRequest ::+  Vault ->+  PortNumber ->+  HalRequest.ProxyRequest a ->+  IO Wai.Request+toWaiRequest vault port req = do+  let pathSegments = T.splitOn "/" . T.dropWhile (== '/') $ HalRequest.path req+      query = constructQuery $ HalRequest.multiValueQueryStringParameters req+      hints =+        NS.defaultHints+          { NS.addrFlags = [NS.AI_NUMERICHOST],+            NS.addrFamily = NS.AF_INET,+            NS.addrSocketType = NS.Stream+          }+      sourceIp =+        T.unpack+          . HalRequest.sourceIp+          . HalRequest.identity+          $ HalRequest.requestContext req+  print $ HalRequest.path req+  print pathSegments+  sourceAddr : _ <- NS.getAddrInfo (Just hints) (Just sourceIp) (Just $ show port)+  body <- returnChunks $ HalRequest.body req+  let waiReq =+        Wai.Request+          { Wai.requestMethod = T.encodeUtf8 $ HalRequest.httpMethod req,+            Wai.httpVersion = HttpVersion 1 1,+            Wai.rawPathInfo =+              BL.toStrict+                . Builder.toLazyByteString+                $ encodePath pathSegments query,+            Wai.rawQueryString = case query of+              [] -> ""+              _ -> renderQuery True query,+            Wai.requestHeaders =+              foldMap+                ( \(hName, hValues) ->+                    (CI.map T.encodeUtf8 hName,) . T.encodeUtf8 <$> hValues+                )+                . H.toList+                $ HalRequest.multiValueHeaders req,+            Wai.isSecure = True,+            Wai.remoteHost = NS.addrAddress sourceAddr,+            Wai.pathInfo = pathSegments,+            Wai.queryString = query,+            Wai.requestBody = body,+            Wai.vault = vault,+            Wai.requestBodyLength =+              Wai.KnownLength . fromIntegral . BL.length $ HalRequest.body req,+            Wai.requestHeaderHost = getHeader hHost req,+            Wai.requestHeaderRange = getHeader hRange req,+            Wai.requestHeaderReferer = getHeader hReferer req,+            Wai.requestHeaderUserAgent = getHeader hUserAgent req+          }+  print waiReq+  pure waiReq++-- | Unpack a lazy 'BL.ByteString' into its chunks, and return an IO+-- action which returns each chunk in sequence, and returns 'B.empty'+-- forever after the bytestring is exhausted.+returnChunks :: BL.ByteString -> IO (IO B.ByteString)+returnChunks bs = do+  chunkRef <- IORef.newIORef $ BL.toChunks bs+  pure . IORef.atomicModifyIORef' chunkRef $+    \case+      [] -> mempty+      (ch : chs) -> (chs, ch)++constructQuery :: HashMap Text [Text] -> Query+constructQuery = foldMap expandParamList . H.toList+  where+    expandParamList :: (Text, [Text]) -> [QueryItem]+    expandParamList (param, values) =+      queryTextToQuery $ case values of+        [] -> [(param, Nothing)]+        _ -> (param,) . Just <$> values++getHeader :: HeaderName -> HalRequest.ProxyRequest a -> Maybe ByteString+getHeader h =+  fmap T.encodeUtf8 . H.lookup (CI.map T.decodeUtf8 h) . HalRequest.headers++-- | Convert a WAI 'Wai.Response' into a hal+-- 'HalResponse.ProxyResponse'.+fromWaiResponse :: Wai.Response -> IO HalResponse.ProxyResponse+fromWaiResponse (Wai.ResponseFile status headers path mFilePart) = do+  fileData <- readFilePart path mFilePart+  pure+    . addHeaders headers+    . HalResponse.response status+    . createProxyBody (getContentType headers)+    $ fileData+fromWaiResponse (Wai.ResponseBuilder status headers builder) =+  pure+    . addHeaders headers+    . HalResponse.response status+    . createProxyBody (getContentType headers)+    . BL.toStrict+    $ Builder.toLazyByteString builder+fromWaiResponse (Wai.ResponseStream status headers stream) = do+  builderRef <- IORef.newIORef mempty+  let addChunk chunk = IORef.modifyIORef builderRef (<> chunk)+      flush = IORef.modifyIORef builderRef (<> Builder.flush)+  stream addChunk flush+  builder <- IORef.readIORef builderRef+  fromWaiResponse (Wai.ResponseBuilder status headers builder)+fromWaiResponse (Wai.ResponseRaw _ resp) = fromWaiResponse resp++readFilePart :: FilePath -> Maybe Wai.FilePart -> IO ByteString+readFilePart path mPart = withFile path ReadMode $ \h -> do+  case mPart of+    Nothing -> B.hGetContents h+    Just (Wai.FilePart offset count _) -> do+      hSeek h AbsoluteSeek offset+      B.hGet h $ fromIntegral count++createProxyBody :: Text -> ByteString -> HalResponse.ProxyBody+createProxyBody contentType body+  | any (`T.isPrefixOf` contentType) ["text/plain", "application/json"] =+    HalResponse.ProxyBody contentType (T.decodeUtf8 body) False+  | otherwise =+    HalResponse.ProxyBody contentType (T.decodeUtf8 $ B64.encode body) True++addHeaders ::+  ResponseHeaders -> HalResponse.ProxyResponse -> HalResponse.ProxyResponse+addHeaders headers response = foldl' addHeader response headers+  where+    addHeader r (hName, hValue) =+      HalResponse.addHeader+        (T.decodeUtf8 $ CI.original hName)+        (T.decodeUtf8 hValue)+        r++-- | Try to find the content-type of a response, given the response+-- headers. If we can't, return @"application/octet-stream"@.+getContentType :: ResponseHeaders -> Text+getContentType =+  maybe "application/octet-stream" T.decodeUtf8 . lookup hContentType
+ wai-handler-hal.cabal view
@@ -0,0 +1,59 @@+cabal-version:      2.2+name:               wai-handler-hal+version:            0.1.0.0+synopsis:           Wrap WAI applications to run on AWS Lambda+description:+  This library provides a function 'Network.Wai.Handler.Hal.run' to+  lift a @wai@ 'Network.Wai.Application' into a function that can be+  passed to @hal@'s 'AWS.Lambda.Runtime.mRuntime'. This allows you to+  run applications written in mature web frameworks (e.g., @servant@)+  on AWS Lambda, as proxy integrations of API Gateway Rest APIs.+  .+  More details, including deployment advice, are available in the+  repository's @README.md@.++homepage:           http://github.com/bellroy/wai-handler-hal+bug-reports:        http://github.com/bellroy/wai-handler-hal/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Bellroy Tech Team <haskell@bellroy.com>+maintainer:         Bellroy Tech Team <haskell@bellroy.com>+copyright:          Copyright (C) 2021 Bellroy Pty Ltd+category:           AWS, Cloud+build-type:         Simple+extra-source-files:+  CHANGELOG.md++tested-with:        GHC ==8.6.5 || ==8.8.4 || ==8.10.4++common opts+  default-language: Haskell2010+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Werror=incomplete-patterns+    -Wredundant-constraints -Wpartial-fields -Wtabs+    -Wmissing-local-signatures -fhelpful-errors+    -fprint-expanded-synonyms -fwarn-unused-do-bind++common deps+  build-depends:+    , base                  >=4.12    && <4.15+    , base64-bytestring     ^>=1.1.0.0+    , bytestring            ^>=0.10.8+    , case-insensitive      ^>=1.2.1.0+    , hal                   ^>=0.4.7+    , http-types            ^>=0.12.3+    , network               ^>=3.1.1.1+    , text                  ^>=1.2.3+    , unordered-containers  ^>=0.2.13+    , vault                 ^>=0.3.1.5+    , wai                   ^>=3.2.3++library+  import:          opts, deps+  exposed-modules: Network.Wai.Handler.Hal+  hs-source-dirs:  src++source-repository head+  type:     git+  location: https://github.com/bellroy/wai-handler-hal.git