1
1
{-# LANGUAGE OverloadedStrings #-}
2
2
{-# LANGUAGE RecordWildCards #-}
3
3
{-# LANGUAGE ScopedTypeVariables #-}
4
- {-# LANGUAGE StandaloneDeriving #-}
4
+ {-# LANGUAGE TypeApplications #-}
5
5
{-# LANGUAGE ViewPatterns #-}
6
6
7
- module Aws.Lambda.Wai (waiHandler , waiHandler' , WaiHandler ) where
7
+ module Aws.Lambda.Wai
8
+ ( runWaiAsLambda ,
9
+ runWaiAsProxiedHttpLambda ,
10
+ WaiLambdaProxyType (.. ),
11
+ apiGatewayWaiHandler ,
12
+ ApiGatewayWaiHandler ,
13
+ albWaiHandler ,
14
+ ALBWaiHandler ,
15
+ ignoreALBPathPart ,
16
+ ignoreNothing ,
17
+ )
18
+ where
8
19
9
20
import Aws.Lambda
10
21
import Control.Concurrent.MVar
11
22
import Data.Aeson
12
- import qualified Data.Aeson as Aeson
13
- import qualified Data.Aeson.Types as Aeson
23
+ import Data.Aeson.Types
24
+ import Data.Bifunctor ( Bifunctor ( bimap ))
14
25
import qualified Data.Binary.Builder as Binary
15
26
import Data.ByteString (ByteString )
16
27
import qualified Data.ByteString as BS
@@ -25,41 +36,171 @@ import qualified Data.Text as T
25
36
import Data.Text.Encoding (decodeUtf8' , encodeUtf8 )
26
37
import qualified Data.Text.Encoding as T
27
38
import qualified Data.Vault.Lazy as Vault
28
- import GHC.IO.Unsafe (unsafePerformIO )
29
39
import qualified Network.HTTP.Types as H
30
40
import qualified Network.Socket as Socket
31
41
import Network.Wai (Application )
32
42
import qualified Network.Wai as Wai
33
43
import qualified Network.Wai.Internal as Wai
44
+ import qualified System.IO as IO
34
45
import Text.Read (readMaybe )
35
46
36
- type WaiHandler context = ApiGatewayRequest Text -> Context context -> IO (Either (ApiGatewayResponse Text ) (ApiGatewayResponse Text ))
47
+ type ApiGatewayWaiHandler = ApiGatewayRequest Text -> Context Application -> IO (Either (ApiGatewayResponse Text ) (ApiGatewayResponse Text ))
37
48
38
- waiHandler :: forall context . IO Wai. Application -> WaiHandler context
39
- waiHandler initApp gatewayRequest context =
40
- initApp
41
- >>= \ app -> waiHandler'' app gatewayRequest context
49
+ type ALBWaiHandler = ALBRequest Text -> Context Application -> IO (Either (ALBResponse Text ) (ALBResponse Text ))
42
50
43
- waiHandler' :: forall context . (context -> Wai. Application ) -> WaiHandler context
44
- waiHandler' getApp request context = do
45
- app <- getApp <$> readIORef (customContext context)
46
- waiHandler'' app request context
51
+ newtype ALBIgnoredPathPortion = ALBIgnoredPathPortion { unALBIgnoredPathPortion :: Text }
47
52
48
- waiHandler'' :: forall context . Wai. Application -> WaiHandler context
49
- waiHandler'' waiApplication gatewayRequest _ = do
50
- waiRequest <- mkWaiRequest gatewayRequest
53
+ data WaiLambdaProxyType
54
+ = APIGateway
55
+ | ALB (Maybe ALBIgnoredPathPortion )
56
+
57
+ runWaiAsProxiedHttpLambda ::
58
+ DispatcherOptions ->
59
+ Maybe ALBIgnoredPathPortion ->
60
+ HandlerName ->
61
+ IO Application ->
62
+ IO ()
63
+ runWaiAsProxiedHttpLambda options ignoredAlbPath handlerName mkApp =
64
+ runLambdaHaskellRuntime options mkApp id $
65
+ addStandaloneLambdaHandler handlerName $ \ (request :: Value ) context ->
66
+ case parse parseIsAlb request of
67
+ Success isAlb -> do
68
+ if isAlb
69
+ then case fromJSON @ (ALBRequest Text ) request of
70
+ Success albRequest ->
71
+ bimap toJSON toJSON <$> albWaiHandler ignoredAlbPath albRequest context
72
+ Error err -> error $ " Could not parse the request as a valid ALB request: " <> err
73
+ else case fromJSON @ (ApiGatewayRequest Text ) request of
74
+ Success apiGwRequest ->
75
+ bimap toJSON toJSON <$> apiGatewayWaiHandler apiGwRequest context
76
+ Error err -> error $ " Could not parse the request as a valid API Gateway request: " <> err
77
+ Error err ->
78
+ error $
79
+ " Could not parse the request as a valid API Gateway or ALB proxy request: " <> err
80
+ where
81
+ parseIsAlb :: Value -> Parser Bool
82
+ parseIsAlb = withObject " Request" $ \ obj -> do
83
+ requestContextMay <- obj .:? " requestContext"
84
+ case requestContextMay of
85
+ Just requestContext -> do
86
+ elb <- requestContext .:? " elb"
87
+ case elb of
88
+ Just (_ :: Value ) -> pure True
89
+ Nothing -> pure False
90
+ Nothing -> pure False
91
+
92
+ runWaiAsLambda ::
93
+ WaiLambdaProxyType ->
94
+ DispatcherOptions ->
95
+ HandlerName ->
96
+ IO Application ->
97
+ IO ()
98
+ runWaiAsLambda proxyType options handlerName mkApp = do
99
+ case proxyType of
100
+ APIGateway -> do
101
+ IO. print $ " Starting Lambda using API gateway handler '" <> unHandlerName handlerName <> " '."
102
+ runLambdaHaskellRuntime options mkApp id $ do
103
+ addAPIGatewayHandler handlerName apiGatewayWaiHandler
104
+ (ALB ignoredPath) -> do
105
+ IO. print $ " Starting Lambda using ALB handler '" <> unHandlerName handlerName <> " '."
106
+ runLambdaHaskellRuntime options mkApp id $ do
107
+ addALBHandler handlerName (albWaiHandler ignoredPath)
108
+
109
+ ignoreALBPathPart :: Text -> Maybe ALBIgnoredPathPortion
110
+ ignoreALBPathPart = Just . ALBIgnoredPathPortion
111
+
112
+ ignoreNothing :: Maybe ALBIgnoredPathPortion
113
+ ignoreNothing = Nothing
114
+
115
+ albWaiHandler :: Maybe ALBIgnoredPathPortion -> ALBWaiHandler
116
+ albWaiHandler ignoredPathPortion request context = do
117
+ waiApplication <- readIORef (customContext context)
118
+ waiRequest <- mkWaiRequestFromALB ignoredPathPortion request
119
+
120
+ (status, headers, body) <- processRequest waiApplication waiRequest >>= readResponse
121
+
122
+ if BS. null body
123
+ then return . pure . mkALBResponse (H. statusCode status) headers $ mempty
124
+ else case decodeUtf8' body of
125
+ Right responseBodyText ->
126
+ return . pure . mkALBResponse (H. statusCode status) headers $ responseBodyText
127
+ Left err -> error $ " Expected a response body that is valid UTF-8: " <> show err
128
+
129
+ apiGatewayWaiHandler :: ApiGatewayWaiHandler
130
+ apiGatewayWaiHandler request context = do
131
+ waiApplication <- readIORef (customContext context)
132
+ waiRequest <- mkWaiRequestFromApiGw request
51
133
52
134
(status, headers, body) <- processRequest waiApplication waiRequest >>= readResponse
53
135
54
136
if BS. null body
55
- then return . pure . wrapInResponse (H. statusCode status) headers $ mempty
137
+ then return . pure . mkApiGatewayResponse (H. statusCode status) headers $ mempty
56
138
else case decodeUtf8' body of
57
139
Right responseBodyText ->
58
- return . pure . wrapInResponse (H. statusCode status) headers $ responseBodyText
59
- Left err -> error " Expected a response body that is valid UTF-8."
140
+ return . pure . mkApiGatewayResponse (H. statusCode status) headers $ responseBodyText
141
+ Left err -> error $ " Expected a response body that is valid UTF-8: " <> show err
142
+
143
+ mkWaiRequestFromALB :: Maybe ALBIgnoredPathPortion -> ALBRequest Text -> IO Wai. Request
144
+ mkWaiRequestFromALB (fmap unALBIgnoredPathPortion -> pathPortionToIgnore) ALBRequest {.. } = do
145
+ let sourceIpMay = albRequestHeaders >>= HMap. lookup " x-forwarded-for"
146
+
147
+ ip <- parseIp sourceIpMay
148
+
149
+ let requestPath =
150
+ case pathPortionToIgnore of
151
+ Just toIgnore ->
152
+ let toIgnoreSafe = " /" <> T. dropWhile (\ c -> c == ' /' || c == ' \\ ' ) toIgnore
153
+ throwPathError =
154
+ error $
155
+ " Given path piece to ignore '"
156
+ <> T. unpack toIgnoreSafe
157
+ <> " ' is longer than the received request path "
158
+ <> T. unpack albRequestPath
159
+ <> " !"
160
+ in fromMaybe throwPathError $ T. stripPrefix toIgnoreSafe albRequestPath
161
+ Nothing -> albRequestPath
162
+
163
+ -- TODO: Duplication
164
+ let pathInfo = H. decodePathSegments (encodeUtf8 requestPath)
165
+
166
+ let requestBodyRaw = maybe mempty T. encodeUtf8 albRequestBody
167
+ let requestBodyLength = Wai. KnownLength $ fromIntegral $ BS. length requestBodyRaw
168
+
169
+ requestBodyMVar <- newMVar requestBodyRaw
170
+
171
+ let requestBody = takeRequestBodyChunk requestBodyMVar
172
+ let headers = fromMaybe HMap. empty albRequestHeaders
173
+ let requestHeaderHost = encodeUtf8 <$> HMap. lookup " host" headers
174
+ let requestHeaderRange = encodeUtf8 <$> HMap. lookup " range" headers
175
+ let requestHeaderReferer = encodeUtf8 <$> HMap. lookup " referer" headers
176
+ let requestHeaderUserAgent = encodeUtf8 <$> HMap. lookup " User-Agent" headers
177
+
178
+ let queryParameters = toQueryStringParameters albRequestQueryStringParameters
179
+ rawQueryString = H. renderQuery True queryParameters
180
+ httpVersion = H. http11 -- ALB converts even HTTP/2 requests to 1.1
181
+ let result =
182
+ Wai. Request
183
+ (encodeUtf8 albRequestHttpMethod)
184
+ httpVersion
185
+ (encodeUtf8 requestPath)
186
+ rawQueryString
187
+ (map toHeader $ HMap. toList headers)
188
+ True -- We assume it's always secure as we're passing through API Gateway
189
+ ip
190
+ pathInfo
191
+ queryParameters
192
+ requestBody
193
+ Vault. empty
194
+ requestBodyLength
195
+ requestHeaderHost
196
+ requestHeaderRange
197
+ requestHeaderReferer
198
+ requestHeaderUserAgent
199
+
200
+ return result
60
201
61
- mkWaiRequest :: ApiGatewayRequest Text -> IO Wai. Request
62
- mkWaiRequest ApiGatewayRequest {.. } = do
202
+ mkWaiRequestFromApiGw :: ApiGatewayRequest Text -> IO Wai. Request
203
+ mkWaiRequestFromApiGw ApiGatewayRequest {.. } = do
63
204
let ApiGatewayRequestContext {.. } = apiGatewayRequestRequestContext
64
205
ApiGatewayRequestContextIdentity {.. } = apiGatewayRequestContextIdentity
65
206
@@ -70,9 +211,9 @@ mkWaiRequest ApiGatewayRequest {..} = do
70
211
-- includes the resource which we don't need
71
212
case apiGatewayRequestPathParameters of
72
213
Just pathParametersMap ->
73
- case HMap. lookup " proxy " pathParametersMap of
74
- Just proxyPath -> proxyPath
75
- Nothing -> apiGatewayRequestPath
214
+ fromMaybe
215
+ apiGatewayRequestPath
216
+ ( HMap. lookup " proxy " pathParametersMap)
76
217
Nothing -> apiGatewayRequestPath
77
218
78
219
let pathInfo = H. decodePathSegments (encodeUtf8 requestPath)
@@ -176,16 +317,5 @@ readResponse (Wai.responseToStream -> (st, hdrs, mkBody)) = do
176
317
(pure () )
177
318
BL. toStrict . Binary. toLazyByteString <$> readIORef ioRef
178
319
179
- wrapInResponse ::
180
- Int ->
181
- H. ResponseHeaders ->
182
- res ->
183
- ApiGatewayResponse res
184
- wrapInResponse code responseHeaders response =
185
- ApiGatewayResponse code responseHeaders response False
186
-
187
320
toHeader :: (Text , Text ) -> H. Header
188
- toHeader (name, val) = (CI. mk . encodeUtf8 $ name, encodeUtf8 val)
189
-
190
- tshow :: Show a => a -> Text
191
- tshow = T. pack . show
321
+ toHeader (name, val) = (CI. mk . encodeUtf8 $ name, encodeUtf8 val)
0 commit comments