Compare commits

...

2 Commits

Author SHA1 Message Date
ef97390a8b Parse the request line properly 2023-09-07 23:39:39 +02:00
7e87f2c8fb Parse full request, including headers and body 2023-09-07 15:18:58 +02:00
10 changed files with 537 additions and 172 deletions

View File

@ -26,6 +26,11 @@ record Monad (M : Type → Type) : Type₁ where
_<$>_ : {A B : Type} (A B) M A M B _<$>_ : {A B : Type} (A B) M A M B
f <$> x = x >>= (λ a pure (f a)) f <$> x = x >>= (λ a pure (f a))
infixl 5 _>>=_
infixl 5 _>>_
infixr 4 _<$>_
open Monad ⦃...⦄ public open Monad ⦃...⦄ public
data Maybe (A : Type) : Type where data Maybe (A : Type) : Type where
@ -38,7 +43,7 @@ _or-else_ : {A : Type} → Maybe A → A → A
(real x) or-else _ = x (real x) or-else _ = x
cake or-else x = x cake or-else x = x
infix 10 _or-else_ infix 1 _or-else_
is-real : {A : Type} Maybe A Type is-real : {A : Type} Maybe A Type
is-real cake = is-real cake =
@ -72,6 +77,18 @@ instance
EqString : Eq Data.String.String EqString : Eq Data.String.String
EqString ._==_ a b = a Data.String.== b EqString ._==_ a b = a Data.String.== b
EqMaybe : {T : Type} Eq T Eq (Maybe T)
EqMaybe ._==_ (real a) (real b) = a == b
EqMaybe ._==_ cake cake = true
EqMaybe ._==_ (real _) cake = false
EqMaybe ._==_ cake (real _) = false
EqList : {T : Type} Eq T Eq (List T)
EqList ._==_ [] [] = true
EqList ._==_ (x xs) (y ys) = x == y xs == ys
EqList ._==_ (_ _) [] = false
EqList ._==_ [] (_ _) = false
record Show (T : Type) : Type₁ where record Show (T : Type) : Type₁ where
field field
show : T String.String show : T String.String

View File

@ -33,15 +33,61 @@ module bytes where
PERCENT = < 37 > PERCENT = < 37 >
BIG = < 0xff > BIG = < 0xff >
private
import Data.String as String
import Data.Fin as Fin
import Data.Char as Char
toHexDigit : String.String
toHexDigit 0 = "0"
toHexDigit 1 = "1"
toHexDigit 2 = "2"
toHexDigit 3 = "3"
toHexDigit 4 = "4"
toHexDigit 5 = "5"
toHexDigit 6 = "6"
toHexDigit 7 = "7"
toHexDigit 8 = "8"
toHexDigit 9 = "9"
toHexDigit 10 = "a"
toHexDigit 11 = "b"
toHexDigit 12 = "c"
toHexDigit 13 = "d"
toHexDigit 14 = "e"
toHexDigit 15 = "f"
toHexDigit x = "?"
toHex : String.String
toHex n = toHexDigit (DivMod.quotient divmod) String.++ toHexDigit (Fin.to (DivMod.remainder divmod))
where
divmod : DivMod n 16
divmod = n divMod 16
instance instance
EqByte : Eq Byte EqByte : Eq Byte
EqByte ._==_ a b = a .value == b .value EqByte ._==_ a b = a .value == b .value
ShowByte : Show Byte ShowByte : Show Byte
ShowByte .show x = "< " String.++ show (x .value) String.++ " >" ShowByte .show x =
if (0x20 ≤ᵇ x .value) (x .value <ᵇ 0x7f) not (x .value == 0x60)
then "`" String.++ String.fromChar (Char.from (x .value)) String.++ "`"
else "<" String.++ toHex (x .value) String.++ ">"
where where
import Data.String as String import Data.String as String
import Data.List as List
ShowByteList : Show (List.List Byte)
ShowByteList .show xs = "'" String.++ show-inner xs String.++ "'"
where
show-inner : List.List Byte String.String
show-inner List.[] = ""
show-inner (x List.∷ xs) =
if (0x20 ≤ᵇ x .value) (x .value <ᵇ 0x7f)
then String.fromChar (Char.from (x .value)) String.++ show-inner xs
else "\\x" String.++ toHex (x .value) String.++ show-inner xs
mkClip : Byte mkClip : Byte
mkClip v = case v <? 256 of λ where mkClip v = case v <? 256 of λ where
(yes v<256) record { value = v ; in-range = v<256 } (yes v<256) record { value = v ; in-range = v<256 }

View File

@ -11,66 +11,105 @@ open L using (List)
open import Data.Product using (_×_; proj₁; proj₂) renaming (_,_ to _,,,_) open import Data.Product using (_×_; proj₁; proj₂) renaming (_,_ to _,,,_)
open import Data.Nat hiding (_<_; _>_) open import Data.Nat hiding (_<_; _>_)
open import Data.Unit using (; tt) open import Data.Unit using (; tt)
open import Data.Bool
open import Base open import Base
open import Bits-and-Bytes using (Byte) open import Bits-and-Bytes using (Byte; ShowByteList)
open import SysIO open import SysIO
open import Socket open import Socket
open import Indexed open import Indexed
open import Parsing (Byte) open import Parsing (Byte)
open import NonEmpty open import NonEmpty
open import Parse-HTTP hiding (_/_) open import Parse-HTTP
open import HTML open import Parse-HTTP.URL hiding (_/_)
open import Parse-HTTP.Methods
import HTML
import UTF-8 import UTF-8
200-ok : S.String List Byte List Byte response-with-content : S.String String List Byte List Byte
200-ok content-type response-data = UTF-8.encode-string (S.toList (headers S.++ "\r\n\r\n")) L.++ response-data response-with-content status-code status content-type response-data =
UTF-8.encode-string (S.toList (status-line S.++ "\r\n" S.++ headers-lines S.++ "\r\n")) L.++ response-data
where where
headers : String status-line : String
headers = "HTTP/1.1 200 OK\r\nContent-Type: " S.++ content-type S.++ "; charset=utf-8\r\nContent-Length: " S.++ (show (L.length response-data)) status-line = "HTTP/1.1 " S.++ show status-code S.++ " " S.++ status
response-headers : List String
response-headers =
("Content-Type: " S.++ content-type S.++ "; charset=utf-8")
L.∷ ("Content-Length: " S.++ (show (L.length response-data)))
L.∷ L.[]
headers-lines : String
headers-lines = L.foldr (λ hdr rest hdr S.++ "\r\n" S.++ rest) "" response-headers
response-without-content : S.String List Byte
response-without-content status-code status =
UTF-8.encode-string (S.toList (
"HTTP/1.1 " S.++ show status-code S.++ " " S.++ status S.++ "\r\n\r\n"
))
200-ok : S.String List Byte List Byte
200-ok content-type response-data = response-with-content 200 "OK" content-type response-data
400-bad-request : List Byte 400-bad-request : List Byte
400-bad-request = UTF-8.encode-string (S.toList "400 Bad Request\r\n\r\n") 400-bad-request = response-without-content 400 "Bad Request"
render-page : (Path × Maybe String) DOM 404-not-found : List Byte
render-page (path ,,, mquery) = 404-not-found = response-with-content 404 "Not Found" "text/plain" (UTF-8.encode-string (S.toList "3: could not find the resource :("))
< html & lang "en" , style "color: yellow; background: black; padding: 1em;" >
< head >
< title > "Test page in Agda" </ title > ,
< meta & name "description" , content "Simple webpage rendered in Agda DSL" /> ,
< meta & name "author" , content "xenia" />
</ head > ,
< body >
< h 1 > "hi" </ h 1 > ,
< p >
"you requested " ,
< span & style "color: white;" > show path </ span > ,
((λ q < span & style "color: fuchsia;" > "?" , q </ span >) <$> mquery or-else L.[])
</ p >
</ body >
</ html >
handle-request : (Path × Maybe String) IO (List Byte) module Pages where
open HTML
render-index : Maybe S.String HTML.DOM
render-index query =
< html & lang "en" , style "color: yellow; background: black; padding: 1em;" >
< head >
< title > "Test page in Agda" </ title > ,
< meta & name "description" , content "Simple webpage rendered in Agda DSL" /> ,
< meta & name "author" , content "xenia" />
</ head > ,
< body >
< h 1 > "hi" </ h 1 > ,
< p >
"welcome to the " ,
< span & style "color: white;" > "index" </ span > ,
(((λ q < span & style "color: fuchsia;" > " (query = " , q , ")" </ span >) <$> query) or-else L.[])
</ p >
</ body >
</ html >
handle-request : Request IO (List Byte)
handle-request req = handle-request req =
pure ( if (req .target .path) == ("index.html" / $)
200-ok then pure (
"text/html" 200-ok
(UTF-8.encode-string (S.toList ( "text/html"
render-dom (render-page req) (UTF-8.encode-string (S.toList (
))) HTML.render-dom (Pages.render-index (req .target .query))
) )))
)
else pure 404-not-found
where
open Request using (target)
open import Parse-HTTP.URL
handle : Socket IO (List Byte) handle : Socket IO (List Byte)
handle sock = do handle sock = do
putStrLn "handle: new connection‼" putStrLn "handle: new connection‼"
let got = get-bytes sock let got = get-bytes sock
let parsed = parse-get-request .parse (V.fromList got) case parse-request .parse (V.fromList got) of λ where
(real req , _ , _ )
case parsed of λ where
(real req@(path ,,, mquery) , _ , _ )
(do (do
putStrLn ("handle: path = " S.++ show path S.++ ", query = " S.++ mquery or-else "(no query)") putStrLn "handle: got request"
putStrLn (" method = " S.++ show (req .method))
putStrLn (" target = " S.++ show (req .target))
putStrLn (" version = " S.++ show (req .version))
putStrLn (" headers = " S.++ show (req .headers))
case req .content of λ where
(real c) putStrLn (" content = " S.++ show ShowByteList c)
cake putStrLn " no content"
handle-request req handle-request req
) )
cake cake
@ -78,6 +117,8 @@ handle sock = do
putStrLn "Got an invalid request" putStrLn "Got an invalid request"
pure 400-bad-request pure 400-bad-request
) )
where
open Request
run-port : run-port :
run-port = 1337 run-port = 1337

View File

@ -1,10 +1,15 @@
module NonEmpty where
open import Data.List using (List; []; _∷_)
open import Agda.Primitive renaming (Set to Type) open import Agda.Primitive renaming (Set to Type)
open import Data.Product
open import Data.Nat
open import Data.List using (List; []; _∷_)
open import Data.Bool
import Data.Vec as V
open import Base open import Base
module NonEmpty where
data List⁺ (A : Type) : Type where data List⁺ (A : Type) : Type where
[_]⁺ : A List⁺ A [_]⁺ : A List⁺ A
_∷⁺_ : A List⁺ A List⁺ A _∷⁺_ : A List⁺ A List⁺ A
@ -23,8 +28,22 @@ list⁺-to-list : {A : Type} → List⁺ A → List A
list⁺-to-list [ x ]⁺ = x [] list⁺-to-list [ x ]⁺ = x []
list⁺-to-list (x ∷⁺ xs) = x list⁺-to-list xs list⁺-to-list (x ∷⁺ xs) = x list⁺-to-list xs
list⁺-to-vec : {A : Type} List⁺ A Σ λ n V.Vec A (suc n)
list⁺-to-vec [ x ]⁺ = 0 , x V.∷ V.[]
list⁺-to-vec (x ∷⁺ xs) = _ , x V.∷ (proj₂ (list⁺-to-vec xs))
vec-to-list⁺ : {A : Type} {n : } V.Vec A (suc n) List⁺ A
vec-to-list⁺ (x V.∷ V.[]) = [ x ]⁺
vec-to-list⁺ (x V.∷ xs@(_ V.∷ _)) = x ∷⁺ vec-to-list⁺ xs
instance instance
ShowList⁺ : {A : Type} Show A Show (List⁺ A) ShowList⁺ : {A : Type} Show A Show (List⁺ A)
ShowList⁺ .show x = show (list⁺-to-list x) String.++ "" ShowList⁺ .show x = show (list⁺-to-list x) String.++ ""
where where
import Data.String as String import Data.String as String
EqList⁺ : {T : Type} Eq T Eq (List⁺ T)
EqList⁺ ._==_ [ x ]⁺ [ y ]⁺ = x == y
EqList⁺ ._==_ (x ∷⁺ xs) (y ∷⁺ ys) = x == y xs == ys
EqList⁺ ._==_ (_ ∷⁺ _) [ _ ]⁺ = false
EqList⁺ ._==_ [ _ ]⁺ (_ ∷⁺ _) = false

View File

@ -1,6 +1,8 @@
{-# OPTIONS --allow-unsolved-metas #-}
open import Agda.Primitive renaming (Set to Type) open import Agda.Primitive renaming (Set to Type)
open import Data.Vec renaming (map to mapᵥ) hiding ([_]; foldr; foldl) open import Data.Vec renaming (map to mapᵥ) hiding ([_]; foldr; foldl; _>>=_)
import Data.List as List import Data.List as List
open import Data.Nat open import Data.Nat
open import Data.Nat.Properties using (<ᵇ⇒<) open import Data.Nat.Properties using (<ᵇ⇒<)
@ -19,103 +21,18 @@ open import Bits-and-Bytes
import UTF-8 import UTF-8
open import Parsing (Byte) open import Parsing (Byte)
open import Parse-HTTP.Methods
open import Parse-HTTP.Helpers open import Parse-HTTP.Helpers
module Parse-HTTP where module Parse-HTTP where
-- HTTP Method: GET, POST, etc. -- helper functions
data HTTP-Method : Type where private
GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE PATCH : HTTP-Method decode-utf8⁺ : List⁺ Byte Maybe String.String
decode-utf8⁺ bs = String.fromList <$> UTF-8.decode-string (list⁺-to-list bs)
instance
ShowMethod : Show HTTP-Method
ShowMethod .show x = go x
where
go : HTTP-Method String.String
go GET = "GET"
go HEAD = "HEAD"
go POST = "POST"
go PUT = "PUT"
go DELETE = "DELETE"
go CONNECT = "CONNECT"
go OPTIONS = "OPTIONS"
go TRACE = "TRACE"
go PATCH = "PATCH"
name-of-enc : HTTP-Method Σ λ n Vec Byte (suc n)
name-of-enc GET = _ , < 71 > < 69 > < 84 > []
name-of-enc HEAD = _ , < 72 > < 69 > < 65 > < 68 > []
name-of-enc POST = _ , < 80 > < 79 > < 83 > < 84 > []
name-of-enc PUT = _ , < 80 > < 85 > < 84 > []
name-of-enc DELETE = _ , < 68 > < 69 > < 76 > < 69 > < 84 > < 69 > []
name-of-enc CONNECT = _ , < 67 > < 79 > < 78 > < 78 > < 69 > < 67 > < 84 > []
name-of-enc OPTIONS = _ , < 79 > < 80 > < 84 > < 73 > < 79 > < 78 > < 83 > []
name-of-enc TRACE = _ , < 84 > < 82 > < 65 > < 67 > < 69 > []
name-of-enc PATCH = _ , < 80 > < 65 > < 84 > < 67 > < 72 > []
http-methods : List⁺ HTTP-Method
http-methods = GET ∷⁺ HEAD ∷⁺ POST ∷⁺ PUT ∷⁺ DELETE ∷⁺ CONNECT ∷⁺ OPTIONS ∷⁺ TRACE ∷⁺ [ PATCH ]⁺
parse-specific-http-method : HTTP-Method [ Parser HTTP-Method ]
parse-specific-http-method m = (λ _ m) <$> exact name
where
name-len :
name-len = proj₁ (name-of-enc m)
name : Vec Byte (suc name-len)
name = proj₂ (name-of-enc m)
parse-name : [ Parser ]
parse-name = exact name
parse-http-method : [ Parser HTTP-Method ]
parse-http-method = foldl (λ p m p <|> (parse-specific-http-method m)) fail http-methods
module Parse-URL where module Parse-URL where
record Authority : Type where open import Parse-HTTP.URL
constructor __
field
host : String.String -- TODO: Split out IP:s as a separate type?
port :
-- TODO: maybe include {port-in-range} : port < 2 ^ 16
open Authority public
infix 10 __
data Path : Type where
$ : Path -- end
_/_ : String.String Path Path
open Path public
instance
ShowPath : Show Path
ShowPath .show $ = "(empty path)"
ShowPath .show (p / $) = p
ShowPath .show (p / rest@(_ / _)) = p String.++ "/" String.++ show rest
infixr 5 _/_
record URL : Type where
constructor http://_/_¿_#_
field
authority : Authority
path : Path
query : Maybe String.String
fragment : Maybe String.String
open URL public
infix 0 http://_/_¿_#_
private
sample-url : URL
sample-url = http:// "coral.shoes" 80 / ("pages" / "index.html" / $) ¿ real "key=value" # cake
-- helper functions
private
decode-utf8⁺ : List⁺ Byte Maybe String.String
decode-utf8⁺ bs = String.fromList <$> UTF-8.decode-string (list⁺-to-list bs)
-- commonly used sub-parsers -- commonly used sub-parsers
private private
@ -138,11 +55,16 @@ module Parse-URL where
reg-name = decode-utf8⁺ <$?> (many (unreserved <|> percent-encoded <|> sub-delims)) reg-name = decode-utf8⁺ <$?> (many (unreserved <|> percent-encoded <|> sub-delims))
unchecked : [ Parser Authority ] unchecked : [ Parser Authority ]
unchecked = (λ (host , rest) host (proj₂ <$> rest or-else 80)) unchecked = (λ (host , rest) host ((proj₂ <$> rest) or-else 80))
<$> (reg-name <&?> enbox (any-of ":" <&> enbox number)) <$> (reg-name <&?> any-of ":" <&>□ number)
parse-path : [ Parser Path ] parse-path : [ Parser Path ]
parse-path = foldr (λ (slash , seg) path seg / path) $ <$> (many (any-of "/" <&> enbox segment)) parse-path =
List.foldr (λ (slash , seg) path seg / path) $ <$>
(
(list⁺-to-list <$> many (any-of "/" <&> enbox segment))
<|> ((λ _ List.[]) <$> any-of "/")
)
where where
segment : [ Parser String.String ] segment : [ Parser String.String ]
segment = decode-utf8⁺ <$?> many pchar segment = decode-utf8⁺ <$?> many pchar
@ -151,35 +73,173 @@ module Parse-URL where
parse-query = decode-utf8⁺ <$?> many (pchar <|> any-of "/?") parse-query = decode-utf8⁺ <$?> many (pchar <|> any-of "/?")
parse-fragment = parse-query parse-fragment = parse-query
parse-path-and-query : [ Parser (Path × Maybe String.String) ] parse-absolute-url : [ Parser (Authority × Path) ]
parse-path-and-query = parse-path <&?>□ (any-of "?" <&⃗>□ parse-query) parse-absolute-url =
(parse-scheme >?= (_== "http"))
<&⃗>□ exact (string-to-ascii-vec "://")
<&⃗>□ parse-authority
<&>□ parse-path
parse-url : [ Parser URL ] parse-url : [ Parser URL ]
parse-url = enurl <$?> parse-parts parse-url =
where parse-absolute-url
enurl : String.String × Authority × (Path × Maybe String.String) × Maybe String.String Maybe URL <&?>□ (any-of "?" <&⃗>□ parse-query)
enurl (scheme , auth , (path , mquery) , mfrag) = >$= λ ((auth , path) , mquery) http:// auth / path ¿ mquery
if true -- scheme == "http"
then real (http:// auth / path ¿ mquery # mfrag)
else cake
parse-parts : [ Parser (String.String × Authority × (Path × Maybe String.String) × Maybe String.String) ]
parse-parts =
parse-scheme <&>□
(any-of ":" <&>□ repeat 2 (any-of "/")) <&⃗>□
parse-authority <&>□
(parse-path <&?>□ (any-of "?" <&⃗>□ parse-query))
<&?>□ (any-of "#" <&⃗>□ parse-fragment)
open Parse-URL public open Parse-URL public
parse-http-version : [ Parser ] module Parse-Header where
parse-http-version = (λ _ tt) <$> (exact (< 72 > < 84 > < 84 > < 80 > < 47 > []) <&>□ many (digit <|> any-of ".")) open import Parse-HTTP.URL using (Authority)
parse-get-request : [ Parser (Path × Maybe String.String) ] record Header : Type where
parse-get-request = field
parse-specific-http-method GET <&⃗>□ name : List⁺ Byte
spaces <&⃗>□ value : List⁺ Byte
parse-path-and-query <&⃖>□ open Header
spaces <&>□
parse-http-version
instance
ShowHeader : Show Header
ShowHeader .show hdr = show ShowByteList (list⁺-to-list (hdr .name)) String.++ ": " String.++ show ShowByteList (list⁺-to-list (hdr .value))
get-field : List⁺ Byte List.List Header Maybe (List⁺ Byte)
get-field field-name List.[] = cake
get-field field-name (hdr List.∷ rest) =
if hdr .name == field-name
then real (hdr .value)
else get-field field-name rest
content-length : List.List Header Maybe
content-length headers =
get-field (string-to-ascii-list⁺ "Content-Length") headers
>>= parse-list⁺ number
host : List.List Header Maybe Authority
host headers =
get-field (string-to-ascii-list⁺ "Host") headers
>>= parse-list⁺ parse-authority
private
parse-vchar-sp : [ Parser Byte ]
parse-vchar-sp = (λ x (0x20 ≤ᵇ x .value) (x .value ≤ᵇ 0x7e)) <?> any₁
parse-token : [ Parser (List⁺ Byte) ]
parse-token = many parse-tchar
where
parse-tchar : [ Parser Byte ]
parse-tchar = digit <|> letter <|> any-of "!#$%&'*+-.^_`|~"
parse-field-content : [ Parser (List⁺ Byte) ]
parse-field-content = many parse-vchar-sp
parse-header : [ Parser Header ]
parse-header =
(λ (name , value) record { name = name ; value = value })
<$> (
parse-token
<&>□ any-of ":"
<&⃗>□ spaces
<&⃗>□ parse-field-content
)
parse-headers⁺ : [ Parser (List⁺ Header) ]
parse-headers⁺ = many parse-header
open Parse-Header public
module Parse-Request where
open import Parse-HTTP.URL
data RequestTargetPath : Type where
OriginForm : Path RequestTargetPath
AbsoluteForm : Authority Path RequestTargetPath
-- We do not care about authority-form nor asterisk form as we don't do any CONNECT nor OPTIONS requests
record RequestTarget : Type where
field
path : RequestTargetPath
query : Maybe String.String
open RequestTarget public
parse-request-target : [ Parser RequestTarget ]
parse-request-target =
(parse-origin-form <|> parse-absolute-form) <&?>□ (any-of "?" <&⃗>□ parse-query)
>$= λ (path , query) record { path = path; query = query }
where
parse-origin-form parse-absolute-form : [ Parser RequestTargetPath ]
parse-origin-form =
parse-path >$= OriginForm
parse-absolute-form =
parse-absolute-url >$= λ (auth , path) AbsoluteForm auth path
data HTTP-Version : Type where
HTTP11 : HTTP-Version -- we only support 1.1
instance
ShowHTTP-Version : Show HTTP-Version
ShowHTTP-Version .show HTTP11 = "HTTP/1.1"
parse-http-version : [ Parser HTTP-Version ]
parse-http-version = (λ _ HTTP11) <$> exact (string-to-ascii-vec "HTTP/1.1")
parse-specific-http-method : HTTP-Method [ Parser HTTP-Method ]
parse-specific-http-method m = (λ _ m) <$> exact (proj₂ (list⁺-to-vec (name-of-method m)))
parse-http-method : [ Parser HTTP-Method ]
parse-http-method = foldr (λ m p parse-specific-http-method m <|> p) fail http-methods
parse-request-line : [ Parser (HTTP-Method × RequestTarget × HTTP-Version) ]
parse-request-line =
parse-http-method
<&>□ (
spaces
<&⃗>□ (
parse-request-target
<&>□ (
spaces
<&⃗>□ parse-http-version
)
)
)
record Request : Type where
field
method : HTTP-Method
target : URL
version : HTTP-Version
headers : List.List Header
content : Maybe (List.List Byte)
parse-request : [ Parser Request ]
parse-request =
((parse-request-line <&⃖>□ crlf) <&>□ many (parse-header <&⃖>□ crlf))
>$?= find-target
>>= λ (method , target , version , headers) enbox (
case content-length (list⁺-to-list headers) of λ where
cake
(
(λ _ record { method = method; target = target; version = version; headers = list⁺-to-list headers; content = cake })
<$> crlf
)
(real 0)
(
(λ _ record { method = method; target = target; version = version; headers = list⁺-to-list headers; content = real List.[] })
<$> (crlf <&>□ crlf)
)
(real n@(suc _))
(
(λ content record { method = method; target = target; version = version; headers = list⁺-to-list headers; content = real (toList content) })
<$> (crlf <&⃗>□ (repeat n any₁))
)
)
where
find-target : ((HTTP-Method × RequestTarget × HTTP-Version) × List⁺ Header) Maybe (HTTP-Method × URL × HTTP-Version × List⁺ Header)
find-target ((method , record { path = OriginForm path ; query = query } , ver) , headers) =
Parse-Header.host (list⁺-to-list headers) >>= λ auth
real (method , (http:// auth / path ¿ query) , ver , headers)
find-target ((method , record { path = AbsoluteForm auth path ; query = query } , ver) , headers) =
real (method , (http:// auth / path ¿ query) , ver , headers)
open Parse-Request public

View File

@ -5,6 +5,9 @@ open import Data.Bool hiding (_<_)
open import Data.Unit open import Data.Unit
open import Data.Vec hiding ([_]; foldl) open import Data.Vec hiding ([_]; foldl)
import Data.List as List import Data.List as List
open import Data.Product
open import Relation.Binary.PropositionalEquality hiding ([_])
import Data.String as String import Data.String as String
import Data.Char as Char import Data.Char as Char
@ -27,6 +30,31 @@ x is c = x .value == Char.to c
_between_and_ : Byte Char.Char Char.Char Bool _between_and_ : Byte Char.Char Char.Char Bool
x between a and b = (Char.to a ≤ᵇ x .value) (x .value ≤ᵇ Char.to b) x between a and b = (Char.to a ≤ᵇ x .value) (x .value ≤ᵇ Char.to b)
list-is-ascii : List.List Char.Char Bool
list-is-ascii List.[] = true
list-is-ascii (c List.∷ chs) = (Char.to c <ᵇ 128) list-is-ascii chs
string-is-ascii : String.String Type
string-is-ascii x = T (list-is-ascii (String.toList x))
string-to-ascii-vec : (s : String.String) {string-is-ascii s} Vec Byte (String.length s)
string-to-ascii-vec x {prf} = go (String.toList x)
where
go : (l : List.List Char.Char) Vec Byte (List.length l)
go List.[] = []
go (ch List.∷ chs) = mkClip (Char.to ch) go chs
string-to-ascii-list⁺ : (s : String.String) {string-is-ascii s} {T (0 <ᵇ String.length s)} List⁺ Byte
string-to-ascii-list⁺ s {ascii} {nonempty} = vec-to-list⁺ v
where
open import Data.Nat.Properties
len≢0 : String.length s 0
len≢0 = m<n⇒n≢0 (<ᵇ⇒< 0 (String.length s) nonempty)
v : Vec Byte (suc (pred (String.length s)))
v = subst (λ x Vec Byte x) (sym (suc[pred[n]]≡n {String.length s} len≢0)) (string-to-ascii-vec s {ascii})
any-of : String.String [ Parser Byte ] any-of : String.String [ Parser Byte ]
any-of x = List.foldr (λ ch p ((_is ch) <?> any₁) <|> p) fail (String.toList x) any-of x = List.foldr (λ ch p ((_is ch) <?> any₁) <|> p) fail (String.toList x)
@ -39,7 +67,7 @@ letter = uppercase <|> lowercase
cr lf crlf : [ Parser ] cr lf crlf : [ Parser ]
cr = (λ _ tt) <$> any-of "\r" cr = (λ _ tt) <$> any-of "\r"
lf = (λ _ tt) <$> any-of "\n" lf = (λ _ tt) <$> any-of "\n"
crlf = cr >> enbox lf crlf = (cr >> enbox lf) <|> lf -- we allow lf because :3
space spaces : [ Parser ] space spaces : [ Parser ]
space = (λ _ tt) <$> any-of " " space = (λ _ tt) <$> any-of " "

View File

@ -0,0 +1,45 @@
open import Agda.Primitive renaming (Set to Type)
import Data.String as String
open import Data.Product
open import Data.Nat
open import Data.Vec
open import Bits-and-Bytes
open import NonEmpty
open import Base
module Parse-HTTP.Methods where
-- HTTP Method: GET, POST, etc.
data HTTP-Method : Type where
GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE PATCH : HTTP-Method
instance
ShowMethod : Show HTTP-Method
ShowMethod .show x = go x
where
go : HTTP-Method String.String
go GET = "GET"
go HEAD = "HEAD"
go POST = "POST"
go PUT = "PUT"
go DELETE = "DELETE"
go CONNECT = "CONNECT"
go OPTIONS = "OPTIONS"
go TRACE = "TRACE"
go PATCH = "PATCH"
name-of-method : HTTP-Method List⁺ Byte
name-of-method GET = < 71 > ∷⁺ < 69 > ∷⁺ [ < 84 > ]⁺
name-of-method HEAD = < 72 > ∷⁺ < 69 > ∷⁺ < 65 > ∷⁺ [ < 68 > ]⁺
name-of-method POST = < 80 > ∷⁺ < 79 > ∷⁺ < 83 > ∷⁺ [ < 84 > ]⁺
name-of-method PUT = < 80 > ∷⁺ < 85 > ∷⁺ [ < 84 > ]⁺
name-of-method DELETE = < 68 > ∷⁺ < 69 > ∷⁺ < 76 > ∷⁺ < 69 > ∷⁺ < 84 > ∷⁺ [ < 69 > ]⁺
name-of-method CONNECT = < 67 > ∷⁺ < 79 > ∷⁺ < 78 > ∷⁺ < 78 > ∷⁺ < 69 > ∷⁺ < 67 > ∷⁺ [ < 84 > ]⁺
name-of-method OPTIONS = < 79 > ∷⁺ < 80 > ∷⁺ < 84 > ∷⁺ < 73 > ∷⁺ < 79 > ∷⁺ < 78 > ∷⁺ [ < 83 > ]⁺
name-of-method TRACE = < 84 > ∷⁺ < 82 > ∷⁺ < 65 > ∷⁺ < 67 > ∷⁺ [ < 69 > ]⁺
name-of-method PATCH = < 80 > ∷⁺ < 65 > ∷⁺ < 84 > ∷⁺ < 67 > ∷⁺ [ < 72 > ]⁺
http-methods : List⁺ HTTP-Method
http-methods = GET ∷⁺ HEAD ∷⁺ POST ∷⁺ PUT ∷⁺ DELETE ∷⁺ CONNECT ∷⁺ OPTIONS ∷⁺ TRACE ∷⁺ [ PATCH ]⁺

View File

@ -9,9 +9,12 @@ open import Base
open import Bits-and-Bytes open import Bits-and-Bytes
open import Parsing open import Parsing
open import UTF-8 open import UTF-8
open import NonEmpty
open import Parse-HTTP open import Parse-HTTP
open import Parse-HTTP.Helpers open import Parse-HTTP.Helpers
open import Parse-HTTP.Methods
open import Parse-HTTP.URL
module Parse-HTTP.Test where module Parse-HTTP.Test where
@ -72,18 +75,40 @@ module Test-HTTP where
cake cake
test-auth-port-out-of-range = refl test-auth-port-out-of-range = refl
test-path : parse-path .parse (enc "/mjau/cat.html?meowing=true") test-path : parse-path .parse (enc "/mjau/cat.html?meowing=true")
real "mjau" / "cat.html" / $ , _ , enc "?meowing=true" real "mjau" / "cat.html" / $ , _ , enc "?meowing=true"
test-path = refl test-path = refl
test-path : parse-path .parse (enc "/%68%65%77%77%6f/%3f%5e%2f%2f%5e%3f?mjau") test-path-encoded : parse-path .parse (enc "/%68%65%77%77%6f/%3f%5e%2f%2f%5e%3f?mjau")
real "hewwo" / "?^//^?" / $ , _ , enc "?mjau" real "hewwo" / "?^//^?" / $ , _ , enc "?mjau"
test-path = refl test-path-encoded = refl
test-url : parse-url .parse (enc "http://www.rfc-editor.org/rfc/rfc3986.html#section-2.1") test-path-bare : parse-path .parse (enc "/")
real http:// "www.rfc-editor.org" 80 / ("rfc" / "rfc3986.html" / $) ¿ cake # real "section-2.1" , _ , enc "" real $ , _ , enc ""
test-path-bare = refl
test-url : parse-url .parse (enc "http://www.rfc-editor.org/rfc/rfc3986.html")
real http:// "www.rfc-editor.org" 80 / ("rfc" / "rfc3986.html" / $) ¿ cake , _ , enc ""
test-url = refl test-url = refl
test-request : parse-get-request .parse (enc "GET /site/index.html?mjau HTTP/1.1\nHeaders...") test-header : parse-header .parse (enc "Content-Length: 6\r\n")
real ( ( "site" / "index.html" / $ ) , real "mjau" ) , _ , enc "\nHeaders..." real record { name = string-to-ascii-list⁺ "Content-Length" ; value = string-to-ascii-list⁺ "6" } , _ , enc "\r\n"
test-header = refl
test-request : parse-request .parse (enc "POST /site/index.html?mjau HTTP/1.1\r\nContent-Length: 6\r\nHost: agda.nu\r\n\r\nmjau:)\r\n")
real
record
{ method = POST
; target = http:// "agda.nu" 80 / ("site" / "index.html" / $) ¿ real "mjau"
; version = HTTP11
; headers =
record { name = string-to-ascii-list⁺ "Content-Length" ; value = string-to-ascii-list⁺ "6" }
List.∷ record { name = string-to-ascii-list⁺ "Host" ; value = string-to-ascii-list⁺ "agda.nu" }
List.∷ List.[]
; content = real (list⁺-to-list (string-to-ascii-list⁺ "mjau:)"))
}
, _
, enc "\r\n"
test-request = refl test-request = refl

64
src/Parse-HTTP/URL.agda Normal file
View File

@ -0,0 +1,64 @@
open import Agda.Primitive renaming (Set to Type)
import Data.String as S
open import Data.Nat
open import Data.Bool
open import Base
module Parse-HTTP.URL where
record Authority : Type where
constructor __
field
host : S.String -- TODO: Split out IP:s as a separate type?
port :
-- TODO: maybe include {port-in-range} : port < 2 ^ 16
open Authority public
instance
ShowAuthority : Show Authority
ShowAuthority .show x =
if x .port == 80
then x .host
else x .host S.++ ":" S.++ show (x .port)
infix 10 __
data Path : Type where
$ : Path -- end
_/_ : S.String Path Path
open Path public
instance
ShowPath : Show Path
ShowPath .show $ = "(empty path)"
ShowPath .show (p / $) = p
ShowPath .show (p / rest@(_ / _)) = p S.++ "/" S.++ show rest
EqPath : Eq Path
EqPath ._==_ $ $ = true
EqPath ._==_ (p / ps) (q / qs) = p == q ps == qs
EqPath ._==_ (_ / _) $ = false
EqPath ._==_ $ (_ / _) = false
infixr 5 _/_
record URL : Type where
constructor http://_/_¿_
field
authority : Authority
path : Path
query : Maybe S.String
open URL public
infix 0 http://_/_¿_
instance
ShowURL : Show URL
ShowURL .show url =
"http://" S.++ show (url .authority) S.++ "/" S.++ show (url .path) S.++ ((λ q "?" S.++ q) <$> url .query or-else "")
private
sample-url : URL
sample-url = http:// "coral.shoes" 80 / ("pages" / "index.html" / $) ¿ real "key=value"

View File

@ -28,8 +28,17 @@ open Parse public
record Parser (A : Type) (n : ) : Type where record Parser (A : Type) (n : ) : Type where
field field
parse : Vec T n Maybe (Parse A n) parse : Vec T n Maybe (Parse A n)
open Parser public open Parser public
parse-list⁺ : {A : Type} [ Parser A ] List⁺ T Maybe A
parse-list⁺ p l =
p .parse (proj₂ (list⁺-to-vec l))
>>= λ parse
if parse .m == 0
then pure (parse .result)
else cake
-- Parser combinators -- Parser combinators
_<$>_ : {A B : Type} (A B) [ Parser A Parser B ] _<$>_ : {A B : Type} (A B) [ Parser A Parser B ]
(f <$> p) .parse a = do (f <$> p) .parse a = do
@ -136,6 +145,17 @@ _>>=_ : {A B : Type} → [ Parser A →′ ((k A) →′ □ Parser B) →
_>>_ : {A B : Type} [ Parser A Parser B Parser B ] _>>_ : {A B : Type} [ Parser A Parser B Parser B ]
pa >> pb = pa >>= λ _ pb pa >> pb = pa >>= λ _ pb
_>$=_ : {A B : Type} [ Parser A k (A B) Parser B ]
pa >$= f = f <$> pa
_>?=_ : {A : Type} [ Parser A k (A Bool) Parser A ]
pa >?= f = f <?> pa
_>$?=_ : {A B : Type} [ Parser A k (A Maybe B) Parser B ]
pa >$?= f = f <$?> pa
infixl 10 _>>=_ _>>_ _>$=_ _>$?=_ _>?=_
fail : {A : Type} [ Parser A ] fail : {A : Type} [ Parser A ]
fail .parse _ = cake fail .parse _ = cake