2 | import public HTTP.API.Server
3 | import public Network.SCGI.Config
5 | import Data.SortedMap as SM
8 | import IO.Async.Loop.Epoll
13 | prettyNS : Integer -> String
14 | prettyNS n = "\{secs}\{msecs}\{usecs}\{nsecs}"
16 | secs, msecs, usecs, nsecs : String
18 | case n `div` 1_000_000_000 of
23 | case n `div` 1_000_000 of
25 | s => "\{show $ s `mod` 1000} ms "
28 | case n `div` 1_000 of
29 | s => "\{show $ s `mod` 1000} us "
31 | nsecs = "\{show $ n `mod` 1000} ns"
37 | largeBody : Nat -> RequestErr
39 | {message := "Maximum content size is \{show n} bytes"} $
40 | requestErr contentTooLarge413
42 | largeHeader : Nat -> RequestErr
44 | {message := "Maximum header size is \{show n} bytes"} $
45 | requestErr requestHeaderFieldsTooLarge431
47 | badRequest : RequestErr
48 | badRequest = requestErr badRequest400
50 | 0 Bytes : List Type -> Type
51 | Bytes es = HTTPStream es ByteString
53 | queryLine : (ByteString,QueryVal) -> String
54 | queryLine (n,QVal v) = "\{n}: \{v}"
55 | queryLine (n,QEmpty) = "\{n}"
61 | CONTENT_LENGTH : String
62 | CONTENT_LENGTH = "CONTENT_LENGTH"
64 | REQUEST_URI : String
65 | REQUEST_URI = "REQUEST_URI"
67 | REQUEST_METHOD : String
68 | REQUEST_METHOD = "REQUEST_METHOD"
70 | CONTENT_TYPE : String
71 | CONTENT_TYPE = "CONTENT_TYPE"
73 | HTTP_PRE : ByteString
76 | adjhname : ByteString -> String
78 | ByteString.toString $
if HTTP_PRE `isPrefixOf` bs then drop (size HTTP_PRE) bs else bs
80 | scgiHeader : String -> ByteString -> Headers -> Headers
82 | if n == CONTENT_LENGTH then insertHeader Content_Length v hs
83 | else if n == CONTENT_TYPE then insertHeader Content_Type v hs
84 | else insertHeader n v hs
86 | parameters {auto conf : Config}
87 | {auto has : Has RequestErr es}
88 | {auto log : HTTPLogger}
90 | contentLength : Headers -> HTTPPull o es Nat
92 | let c := contentLength hs
93 | in case c > conf.maxMsgSize of
95 | True => throw (largeBody conf.maxMsgSize)
97 | parseRequestURI : Headers -> HTTPPull o es URI
98 | parseRequestURI hs =
99 | maybe (throw badRequest) requestURI $
100 | lookupUpperCaseHeader REQUEST_URI hs
102 | parseRequestMethod : Headers -> HTTPPull o es Method
103 | parseRequestMethod hs =
104 | maybe (throw badRequest) requestMethod $
105 | lookupUpperCaseHeader REQUEST_METHOD hs
111 | headerSize : Bytes es -> HTTPPull o es (Nat, Bytes es)
113 | C.forceBreakFull badRequest DropHit (58 ==) p
114 | |> C.limit (largeHeader conf.maxHeaderSize) 10
115 | |> P.foldPair (<+>) empty
116 | |> map (mapFst $
fromMaybe 0 . ByteString.parseDecimalNat)
119 | header : Nat -> Bytes es -> HTTPPull o es (Headers, Bytes es)
123 | |> P.observe (\xs => traceML $
map (\x => "Header part: \{x}") xs)
124 | |> P.foldPair (++) []
125 | |> map (mapFst $
go emptyHeaders)
128 | go : Headers -> List ByteString -> Headers
129 | go hs (x::y::t) = go (scgiHeader (adjhname x) y hs) t
132 | request : Bytes es -> HTTPPull o es Request
133 | request p = Prelude.do
134 | (hsz, rem1) <- headerSize p
135 | when (hsz > conf.maxHeaderSize) (throw $
largeHeader conf.maxHeaderSize)
136 | (head,rem2) <- header hsz rem1
137 | exec $
debugML ((\(k,v) => "\{k}: \{v}") <$> kvList head)
138 | cl <- contentLength head
139 | m <- parseRequestMethod head
140 | u <- parseRequestURI head
141 | exec $
info "Got a \{show m} request at \{encodePath u} (\{show cl} bytes)"
142 | exec $
debugML ("queries:" :: map queryLine u.queries)
143 | body <- foldGet (:<) [<] (C.take cl $
C.drop 1 rem2)
144 | pure $
RQ m head u (fastConcat $
body <>> [])
146 | logErr : HTTPLogger => RequestErr -> HTTPPull o es ()
147 | logErr (RE s e m d p) =
148 | exec $
if "" == p then warnML msgLines else infoML msgLines
150 | msg, dts : List String
151 | msg = if "" == m then [] else ["message: \{m}"]
155 | _ => "details:" :: map (indent 2) (String.lines d)
157 | msgLines : List String
159 | let u := if "" == p then "" else "at \{p}"
160 | m := "invalid request \{u} (status code \{show s}): \{e}"
163 | parameters {auto log : HTTPLogger}
174 | serveStream : Config -> (Request -> Handler Response) -> HTTPStream [] Void
175 | serveStream c@(C a p _ _ co) run =
177 | parBind co doServe (acceptOn AF_INET SOCK_STREAM $
IP4 a p)
180 | handlers : All (\e => e -> HTTPPull Void [] ()) [Errno]
181 | handlers = [exec . ierror]
184 | request' : Socket AF_INET -> HTTPPull o [RequestErr,Errno] Request
185 | request' cli = request $
bytes cli 0xffff
187 | send : Socket AF_INET -> Response -> HTTPStream [Errno] Void
188 | send cli resp = writeTo cli (emits $
responseBytes resp)
190 | doServe : Socket AF_INET -> HTTPStream [Errno] Void
191 | doServe cli = weakenErrors $ Prelude.do
192 | c1 <- liftIO $
clockTime Monotonic
193 | d <- finally (close' cli) $
handle handlers $
194 | extractErr RequestErr (request' cli) >>= \case
195 | Left x => logErr x >> send cli (fromError Nothing emptyHeaders x)
196 | Right req => exec (weakenErrors $
extractErr RequestErr $
run req) >>= \case
197 | Left x => logErr x >> send cli (fromError (Just req.uri) req.headers x)
198 | Right resp => send cli resp
199 | c2 <- liftIO $
clockTime Monotonic
200 | exec $
debug "request served in \{prettyNS $ toNano $ timeDifference c2 c1}"
209 | serve : Config -> (Request -> Handler Response) -> HTTPProg [] ()
210 | serve c run = mpull (serveStream c run)
218 | serveIO : Config -> (Request -> IO Response) -> IO ()
219 | serveIO c run = simpleApp (serve c $
liftIO . run)