aws-lambda-haskell-runtime-wai 1.0.2 → 2.0.0
raw patch · 3 files changed
+257/−102 lines, 3 filesdep ~aws-lambda-haskell-runtimePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: aws-lambda-haskell-runtime
API changes (from Hackage documentation)
- Aws.Lambda.Wai: type WaiHandler context = ApiGatewayRequest Text -> Context context -> IO (Either (ApiGatewayResponse Text) (ApiGatewayResponse Text))
- Aws.Lambda.Wai: waiHandler :: forall context. IO Application -> WaiHandler context
- Aws.Lambda.Wai: waiHandler' :: forall context. (context -> Application) -> WaiHandler context
+ Aws.Lambda.Wai: ALB :: Maybe ALBIgnoredPathPortion -> WaiLambdaProxyType
+ Aws.Lambda.Wai: APIGateway :: WaiLambdaProxyType
+ Aws.Lambda.Wai: albWaiHandler :: Maybe ALBIgnoredPathPortion -> ALBWaiHandler
+ Aws.Lambda.Wai: apiGatewayWaiHandler :: ApiGatewayWaiHandler
+ Aws.Lambda.Wai: data WaiLambdaProxyType
+ Aws.Lambda.Wai: ignoreALBPathPart :: Text -> Maybe ALBIgnoredPathPortion
+ Aws.Lambda.Wai: ignoreNothing :: Maybe ALBIgnoredPathPortion
+ Aws.Lambda.Wai: runWaiAsLambda :: WaiLambdaProxyType -> DispatcherOptions -> HandlerName -> IO Application -> IO ()
+ Aws.Lambda.Wai: runWaiAsProxiedHttpLambda :: DispatcherOptions -> Maybe ALBIgnoredPathPortion -> HandlerName -> IO Application -> IO ()
+ Aws.Lambda.Wai: type ALBWaiHandler = ALBRequest Text -> Context Application -> IO (Either (ALBResponse Text) (ALBResponse Text))
+ Aws.Lambda.Wai: type ApiGatewayWaiHandler = ApiGatewayRequest Text -> Context Application -> IO (Either (ApiGatewayResponse Text) (ApiGatewayResponse Text))
Files
- ChangeLog.md +9/−0
- aws-lambda-haskell-runtime-wai.cabal +5/−4
- src/Aws/Lambda/Wai.hs +243/−98
ChangeLog.md view
@@ -1,5 +1,14 @@ # Changelog for aws-lambda-haskell-runtime-wai +## 2.0.0++* Using version [`4.0.0`](https://github.com/theam/aws-lambda-haskell-runtime/pull/97) of `aws-lambda-haskell-runtime`.+* New handler types that allow you to support ALB or even API Gateway + ALB at once.++## 1.0.3++* Now taking the Wai request path from API Gateway's 'proxy' path parameter. This prevents the resource path from messing up your Wai application routing.+ ## 1.0.2 * Switched to aws-lambda-haskell-runtime 3.0.3
aws-lambda-haskell-runtime-wai.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 56197b54e09e2b69cdedd41eb41de7cbdfbc11cdefef8f57e1141ca11652b166+-- hash: 6ca9eaab6b86b712b823d22c59d7e6bf2ab727e70e373b7ad6be7230c7084187 name: aws-lambda-haskell-runtime-wai-version: 1.0.2+version: 2.0.0 synopsis: Run wai applications on AWS Lambda description: Please see the README on GitHub at <https://github.com/eir-forsakring/aws-lambda-haskell-runtime-wai#readme> category: AWS@@ -34,9 +34,10 @@ Paths_aws_lambda_haskell_runtime_wai hs-source-dirs: src+ ghc-options: -Wall build-depends: aeson- , aws-lambda-haskell-runtime >=3.0.0+ , aws-lambda-haskell-runtime >=4.0.0 , base >=4.7 && <5 , binary , bytestring@@ -60,7 +61,7 @@ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: aeson- , aws-lambda-haskell-runtime >=3.0.0+ , aws-lambda-haskell-runtime >=4.0.0 , aws-lambda-haskell-runtime-wai , base >=4.7 && <5 , binary
src/Aws/Lambda/Wai.hs view
@@ -1,101 +1,257 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-} -module Aws.Lambda.Wai (waiHandler, waiHandler', WaiHandler) where+module Aws.Lambda.Wai+ ( runWaiAsLambda,+ runWaiAsProxiedHttpLambda,+ WaiLambdaProxyType (..),+ apiGatewayWaiHandler,+ ApiGatewayWaiHandler,+ albWaiHandler,+ ALBWaiHandler,+ ignoreALBPathPart,+ ignoreNothing,+ )+where -import Aws.Lambda-import Control.Concurrent.MVar-import Data.Aeson-import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Types as Aeson-import qualified Data.Binary.Builder as Binary-import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BL-import qualified Data.CaseInsensitive as CI-import qualified Data.HashMap.Strict as HMap-import Data.IORef-import qualified Data.IP as IP-import Data.Text (Text)-import qualified Data.Text as T-import Data.Text.Encoding (decodeUtf8', encodeUtf8)-import qualified Data.Text.Encoding as T-import qualified Data.Vault.Lazy as Vault-import GHC.IO.Unsafe (unsafePerformIO)-import qualified Network.HTTP.Types as H-import qualified Network.Socket as Socket-import Network.Wai (Application)-import qualified Network.Wai as Wai-import qualified Network.Wai.Internal as Wai-import Text.Read (readMaybe)+import Aws.Lambda+import Control.Concurrent.MVar+import Data.Aeson+import Data.Aeson.Types+import Data.Bifunctor (Bifunctor (bimap))+import qualified Data.Binary.Builder as Binary+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.CaseInsensitive as CI+import qualified Data.HashMap.Strict as HMap+import Data.IORef+import qualified Data.IP as IP+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8', encodeUtf8)+import qualified Data.Text.Encoding as T+import qualified Data.Vault.Lazy as Vault+import qualified Network.HTTP.Types as H+import qualified Network.Socket as Socket+import Network.Wai (Application)+import qualified Network.Wai as Wai+import qualified Network.Wai.Internal as Wai+import qualified System.IO as IO+import Text.Read (readMaybe) -type WaiHandler context = ApiGatewayRequest Text -> Context context -> IO (Either (ApiGatewayResponse Text) (ApiGatewayResponse Text))+type ApiGatewayWaiHandler = ApiGatewayRequest Text -> Context Application -> IO (Either (ApiGatewayResponse Text) (ApiGatewayResponse Text)) -waiHandler :: forall context. IO Wai.Application -> WaiHandler context-waiHandler initApp gatewayRequest context = initApp >>=- \app -> waiHandler'' app gatewayRequest context+type ALBWaiHandler = ALBRequest Text -> Context Application -> IO (Either (ALBResponse Text) (ALBResponse Text)) -waiHandler' :: forall context. (context -> Wai.Application) -> WaiHandler context-waiHandler' getApp request context = do- app <- getApp <$> readIORef (customContext context)- waiHandler'' app request context+newtype ALBIgnoredPathPortion = ALBIgnoredPathPortion {unALBIgnoredPathPortion :: Text} -waiHandler'' :: forall context. Wai.Application -> WaiHandler context-waiHandler'' waiApplication gatewayRequest _ = do- waiRequest <- mkWaiRequest gatewayRequest+data WaiLambdaProxyType+ = APIGateway+ | ALB (Maybe ALBIgnoredPathPortion) +runWaiAsProxiedHttpLambda ::+ DispatcherOptions ->+ Maybe ALBIgnoredPathPortion ->+ HandlerName ->+ IO Application ->+ IO ()+runWaiAsProxiedHttpLambda options ignoredAlbPath handlerName mkApp =+ runLambdaHaskellRuntime options mkApp id $+ addStandaloneLambdaHandler handlerName $ \(request :: Value) context ->+ case parse parseIsAlb request of+ Success isAlb -> do+ if isAlb+ then case fromJSON @(ALBRequest Text) request of+ Success albRequest ->+ bimap toJSON toJSON <$> albWaiHandler ignoredAlbPath albRequest context+ Error err -> error $ "Could not parse the request as a valid ALB request: " <> err+ else case fromJSON @(ApiGatewayRequest Text) request of+ Success apiGwRequest ->+ bimap toJSON toJSON <$> apiGatewayWaiHandler apiGwRequest context+ Error err -> error $ "Could not parse the request as a valid API Gateway request: " <> err+ Error err ->+ error $+ "Could not parse the request as a valid API Gateway or ALB proxy request: " <> err+ where+ parseIsAlb :: Value -> Parser Bool+ parseIsAlb = withObject "Request" $ \obj -> do+ requestContextMay <- obj .:? "requestContext"+ case requestContextMay of+ Just requestContext -> do+ elb <- requestContext .:? "elb"+ case elb of+ Just (_ :: Value) -> pure True+ Nothing -> pure False+ Nothing -> pure False++runWaiAsLambda ::+ WaiLambdaProxyType ->+ DispatcherOptions ->+ HandlerName ->+ IO Application ->+ IO ()+runWaiAsLambda proxyType options handlerName mkApp = do+ case proxyType of+ APIGateway -> do+ IO.print $ "Starting Lambda using API gateway handler '" <> unHandlerName handlerName <> "'."+ runLambdaHaskellRuntime options mkApp id $ do+ addAPIGatewayHandler handlerName apiGatewayWaiHandler+ (ALB ignoredPath) -> do+ IO.print $ "Starting Lambda using ALB handler '" <> unHandlerName handlerName <> "'."+ runLambdaHaskellRuntime options mkApp id $ do+ addALBHandler handlerName (albWaiHandler ignoredPath)++ignoreALBPathPart :: Text -> Maybe ALBIgnoredPathPortion+ignoreALBPathPart = Just . ALBIgnoredPathPortion++ignoreNothing :: Maybe ALBIgnoredPathPortion+ignoreNothing = Nothing++albWaiHandler :: Maybe ALBIgnoredPathPortion -> ALBWaiHandler+albWaiHandler ignoredPathPortion request context = do+ waiApplication <- readIORef (customContext context)+ waiRequest <- mkWaiRequestFromALB ignoredPathPortion request+ (status, headers, body) <- processRequest waiApplication waiRequest >>= readResponse if BS.null body- then return . pure . wrapInResponse (H.statusCode status) headers $ mempty- else case decodeUtf8' body of- Right responseBodyText ->- return . pure . wrapInResponse (H.statusCode status) headers $ responseBodyText- Left err -> error "Expected a response body that is valid UTF-8."+ then return . pure . mkALBResponse (H.statusCode status) headers $ mempty+ else case decodeUtf8' body of+ Right responseBodyText ->+ return . pure . mkALBResponse (H.statusCode status) headers $ responseBodyText+ Left err -> error $ "Expected a response body that is valid UTF-8: " <> show err -mkWaiRequest :: ApiGatewayRequest Text -> IO Wai.Request-mkWaiRequest ApiGatewayRequest{..} = do- let ApiGatewayRequestContext{..} = apiGatewayRequestRequestContext- ApiGatewayRequestContextIdentity{..} = apiGatewayRequestContextIdentity+apiGatewayWaiHandler :: ApiGatewayWaiHandler+apiGatewayWaiHandler request context = do+ waiApplication <- readIORef (customContext context)+ waiRequest <- mkWaiRequestFromApiGw request + (status, headers, body) <- processRequest waiApplication waiRequest >>= readResponse++ if BS.null body+ then return . pure . mkApiGatewayResponse (H.statusCode status) headers $ mempty+ else case decodeUtf8' body of+ Right responseBodyText ->+ return . pure . mkApiGatewayResponse (H.statusCode status) headers $ responseBodyText+ Left err -> error $ "Expected a response body that is valid UTF-8: " <> show err++mkWaiRequestFromALB :: Maybe ALBIgnoredPathPortion -> ALBRequest Text -> IO Wai.Request+mkWaiRequestFromALB (fmap unALBIgnoredPathPortion -> pathPortionToIgnore) ALBRequest {..} = do+ let sourceIpMay = albRequestHeaders >>= HMap.lookup "x-forwarded-for"++ ip <- parseIp sourceIpMay++ let requestPath =+ case pathPortionToIgnore of+ Just toIgnore ->+ let toIgnoreSafe = "/" <> T.dropWhile (\c -> c == '/' || c == '\\') toIgnore+ throwPathError =+ error $+ "Given path piece to ignore '"+ <> T.unpack toIgnoreSafe+ <> "' is longer than the received request path "+ <> T.unpack albRequestPath+ <> "!"+ in fromMaybe throwPathError $ T.stripPrefix toIgnoreSafe albRequestPath+ Nothing -> albRequestPath++ -- TODO: Duplication+ let pathInfo = H.decodePathSegments (encodeUtf8 requestPath)++ let requestBodyRaw = maybe mempty T.encodeUtf8 albRequestBody+ let requestBodyLength = Wai.KnownLength $ fromIntegral $ BS.length requestBodyRaw++ requestBodyMVar <- newMVar requestBodyRaw++ let requestBody = takeRequestBodyChunk requestBodyMVar+ let headers = fromMaybe HMap.empty albRequestHeaders+ let requestHeaderHost = encodeUtf8 <$> HMap.lookup "host" headers+ let requestHeaderRange = encodeUtf8 <$> HMap.lookup "range" headers+ let requestHeaderReferer = encodeUtf8 <$> HMap.lookup "referer" headers+ let requestHeaderUserAgent = encodeUtf8 <$> HMap.lookup "User-Agent" headers++ let queryParameters = toQueryStringParameters albRequestQueryStringParameters+ rawQueryString = H.renderQuery True queryParameters+ httpVersion = H.http11 -- ALB converts even HTTP/2 requests to 1.1+ let result =+ Wai.Request+ (encodeUtf8 albRequestHttpMethod)+ httpVersion+ (encodeUtf8 requestPath)+ rawQueryString+ (map toHeader $ HMap.toList headers)+ True -- We assume it's always secure as we're passing through API Gateway+ ip+ pathInfo+ queryParameters+ requestBody+ Vault.empty+ requestBodyLength+ requestHeaderHost+ requestHeaderRange+ requestHeaderReferer+ requestHeaderUserAgent++ return result++mkWaiRequestFromApiGw :: ApiGatewayRequest Text -> IO Wai.Request+mkWaiRequestFromApiGw ApiGatewayRequest {..} = do+ let ApiGatewayRequestContext {..} = apiGatewayRequestRequestContext+ ApiGatewayRequestContextIdentity {..} = apiGatewayRequestContextIdentity+ ip <- parseIp apiGatewayRequestContextIdentitySourceIp - let pathInfo = H.decodePathSegments (encodeUtf8 apiGatewayRequestPath)+ let requestPath =+ -- We prefer the proxied path because apiGatewayRequestPath also+ -- includes the resource which we don't need+ case apiGatewayRequestPathParameters of+ Just pathParametersMap ->+ fromMaybe+ apiGatewayRequestPath+ (HMap.lookup "proxy" pathParametersMap)+ Nothing -> apiGatewayRequestPath + let pathInfo = H.decodePathSegments (encodeUtf8 requestPath)+ let requestBodyRaw = maybe mempty T.encodeUtf8 apiGatewayRequestBody let requestBodyLength = Wai.KnownLength $ fromIntegral $ BS.length requestBodyRaw requestBodyMVar <- newMVar requestBodyRaw let requestBody = takeRequestBodyChunk requestBodyMVar- let requestHeaderHost = encodeUtf8 <$> HMap.lookup "host" apiGatewayRequestHeaders- let requestHeaderRange = encodeUtf8 <$> HMap.lookup "range" apiGatewayRequestHeaders- let requestHeaderReferer = encodeUtf8 <$> HMap.lookup "referer" apiGatewayRequestHeaders- let requestHeaderUserAgent = encodeUtf8 <$> HMap.lookup "User-Agent" apiGatewayRequestHeaders+ let headers = fromMaybe HMap.empty apiGatewayRequestHeaders+ let requestHeaderHost = encodeUtf8 <$> HMap.lookup "host" headers+ let requestHeaderRange = encodeUtf8 <$> HMap.lookup "range" headers+ let requestHeaderReferer = encodeUtf8 <$> HMap.lookup "referer" headers+ let requestHeaderUserAgent = encodeUtf8 <$> HMap.lookup "User-Agent" headers let queryParameters = toQueryStringParameters apiGatewayRequestQueryStringParameters rawQueryString = H.renderQuery True queryParameters httpVersion = getHttpVersion apiGatewayRequestContextProtocol - let result = Wai.Request- (encodeUtf8 apiGatewayRequestHttpMethod)- httpVersion- (encodeUtf8 apiGatewayRequestPath)- rawQueryString- (map toHeader $ HMap.toList apiGatewayRequestHeaders)- True -- We assume it's always secure as we're passing through API Gateway- ip- pathInfo- queryParameters- requestBody- Vault.empty- requestBodyLength- requestHeaderHost- requestHeaderRange- requestHeaderReferer- requestHeaderUserAgent+ let result =+ Wai.Request+ (encodeUtf8 apiGatewayRequestHttpMethod)+ httpVersion+ (encodeUtf8 requestPath)+ rawQueryString+ (map toHeader $ HMap.toList headers)+ True -- We assume it's always secure as we're passing through API Gateway+ ip+ pathInfo+ queryParameters+ requestBody+ Vault.empty+ requestBodyLength+ requestHeaderHost+ requestHeaderRange+ requestHeaderReferer+ requestHeaderUserAgent return result @@ -117,7 +273,7 @@ toQueryStringParameters :: Maybe (HMap.HashMap Text Text) -> [H.QueryItem] toQueryStringParameters (Just params) = let toQueryItem (key, value) = (encodeUtf8 key, Just $ encodeUtf8 value)- in map toQueryItem $ HMap.toList params+ in map toQueryItem $ HMap.toList params toQueryStringParameters _ = [] parseIp :: Maybe Text -> IO Socket.SockAddr@@ -125,19 +281,19 @@ case sourceIpText of Just sourceIp -> case readMaybe (T.unpack sourceIp) of- Just ip ->- pure $ case ip of- IP.IPv4 ip4 ->- Socket.SockAddrInet- 0 -- default port- (IP.toHostAddress ip4)- IP.IPv6 ip6 ->- Socket.SockAddrInet6- 0 -- default port- 0 -- flow info- (IP.toHostAddress6 ip6)- 0 -- scope id- Nothing -> error "Could not parse source ip."+ Just ip ->+ pure $ case ip of+ IP.IPv4 ip4 ->+ Socket.SockAddrInet+ 0 -- default port+ (IP.toHostAddress ip4)+ IP.IPv6 ip6 ->+ Socket.SockAddrInet6+ 0 -- default port+ 0 -- flow info+ (IP.toHostAddress6 ip6)+ 0 -- scope id+ Nothing -> error "Could not parse source ip." Nothing -> error "Missing source ip." processRequest :: Application -> Wai.Request -> IO Wai.Response@@ -150,8 +306,8 @@ readResponse :: Wai.Response -> IO (H.Status, H.ResponseHeaders, ByteString) readResponse (Wai.responseToStream -> (st, hdrs, mkBody)) = do- body <- mkBody drainBody- pure (st, hdrs, body)+ body <- mkBody drainBody+ pure (st, hdrs, body) where drainBody :: Wai.StreamingBody -> IO ByteString drainBody body = do@@ -161,16 +317,5 @@ (pure ()) BL.toStrict . Binary.toLazyByteString <$> readIORef ioRef -wrapInResponse- :: Int- -> H.ResponseHeaders- -> res- -> ApiGatewayResponse res-wrapInResponse code responseHeaders response =- ApiGatewayResponse code responseHeaders response False- toHeader :: (Text, Text) -> H.Header toHeader (name, val) = (CI.mk . encodeUtf8 $ name, encodeUtf8 val)--tshow :: Show a => a -> Text-tshow = T.pack . show