Skip to content

Instantly share code, notes, and snippets.

@mratsim
Forked from xmonader/myproj.nim
Last active October 23, 2019 19:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mratsim/3e98f5fb5ab58118213c80b7172a5e9a to your computer and use it in GitHub Desktop.
Save mratsim/3e98f5fb5ab58118213c80b7172a5e9a to your computer and use it in GitHub Desktop.
myproj.nim
# This is just an example to get you started. A typical binary package
# uses this file as the main entry point of the application.
import strformat, tables, json, strutils, asyncdispatch, asyncnet, strutils, parseutils, options, net
from cgi import decodeUrl
import terminaltables
import mimetypes
type
HttpVersion* = enum
HttpVer11,
HttpVer10
HttpMethod* = enum ## the requested HttpMethod
HttpHead, ## Asks for the response identical to the one that would
## correspond to a GET request, but without the response
## body.
HttpGet, ## Retrieves the specified resource.
HttpPost, ## Submits data to be processed to the identified
## resource. The data is included in the body of the
## request.
HttpPut, ## Uploads a representation of the specified resource.
HttpDelete, ## Deletes the specified resource.
HttpTrace, ## Echoes back the received request, so that a client
## can see what intermediate servers are adding or
## changing in the request.
HttpOptions, ## Returns the HTTP methods that the server supports
## for specified address.
HttpConnect, ## Converts the request connection to a transparent
## TCP/IP tunnel, usually used for proxies.
HttpPatch ## Applies partial modifications to a resource.
HttpCode* = distinct range[0 .. 599]
const
Http100* = HttpCode(100)
Http101* = HttpCode(101)
Http200* = HttpCode(200)
Http201* = HttpCode(201)
Http202* = HttpCode(202)
Http203* = HttpCode(203)
Http204* = HttpCode(204)
Http205* = HttpCode(205)
Http206* = HttpCode(206)
Http300* = HttpCode(300)
Http301* = HttpCode(301)
Http302* = HttpCode(302)
Http303* = HttpCode(303)
Http304* = HttpCode(304)
Http305* = HttpCode(305)
Http307* = HttpCode(307)
Http400* = HttpCode(400)
Http401* = HttpCode(401)
Http403* = HttpCode(403)
Http404* = HttpCode(404)
Http405* = HttpCode(405)
Http406* = HttpCode(406)
Http407* = HttpCode(407)
Http408* = HttpCode(408)
Http409* = HttpCode(409)
Http410* = HttpCode(410)
Http411* = HttpCode(411)
Http412* = HttpCode(412)
Http413* = HttpCode(413)
Http414* = HttpCode(414)
Http415* = HttpCode(415)
Http416* = HttpCode(416)
Http417* = HttpCode(417)
Http418* = HttpCode(418)
Http421* = HttpCode(421)
Http422* = HttpCode(422)
Http426* = HttpCode(426)
Http428* = HttpCode(428)
Http429* = HttpCode(429)
Http431* = HttpCode(431)
Http451* = HttpCode(451)
Http500* = HttpCode(500)
Http501* = HttpCode(501)
Http502* = HttpCode(502)
Http503* = HttpCode(503)
Http504* = HttpCode(504)
Http505* = HttpCode(505)
proc `$`*(code: HttpCode): string =
## Converts the specified ``HttpCode`` into a HTTP status.
##
## For example:
##
## .. code-block:: nim
## doAssert($Http404 == "404 Not Found")
case code.int
of 100: "100 Continue"
of 101: "101 Switching Protocols"
of 200: "200 OK"
of 201: "201 Created"
of 202: "202 Accepted"
of 203: "203 Non-Authoritative Information"
of 204: "204 No Content"
of 205: "205 Reset Content"
of 206: "206 Partial Content"
of 300: "300 Multiple Choices"
of 301: "301 Moved Permanently"
of 302: "302 Found"
of 303: "303 See Other"
of 304: "304 Not Modified"
of 305: "305 Use Proxy"
of 307: "307 Temporary Redirect"
of 400: "400 Bad Request"
of 401: "401 Unauthorized"
of 403: "403 Forbidden"
of 404: "404 Not Found"
of 405: "405 Method Not Allowed"
of 406: "406 Not Acceptable"
of 407: "407 Proxy Authentication Required"
of 408: "408 Request Timeout"
of 409: "409 Conflict"
of 410: "410 Gone"
of 411: "411 Length Required"
of 412: "412 Precondition Failed"
of 413: "413 Request Entity Too Large"
of 414: "414 Request-URI Too Long"
of 415: "415 Unsupported Media Type"
of 416: "416 Requested Range Not Satisfiable"
of 417: "417 Expectation Failed"
of 418: "418 I'm a teapot"
of 421: "421 Misdirected Request"
of 422: "422 Unprocessable Entity"
of 426: "426 Upgrade Required"
of 428: "428 Precondition Required"
of 429: "429 Too Many Requests"
of 431: "431 Request Header Fields Too Large"
of 451: "451 Unavailable For Legal Reasons"
of 500: "500 Internal Server Error"
of 501: "501 Not Implemented"
of 502: "502 Bad Gateway"
of 503: "503 Service Unavailable"
of 504: "504 Gateway Timeout"
of 505: "505 HTTP Version Not Supported"
of 506: "506 Variant Also Negotiates"
of 507: "507 Insufficient Storage"
of 508: "508 Loop Detected"
of 510: "510 Not Extended"
of 511: "511 Network Authentication Required"
of 599: "599 Network Connect Timeout Error"
else: $(int(code))
const headerLimit* = 10_000
type HttpHeaders* = ref object
table*: TableRef[string, seq[string]]
type HttpHeaderValues* = seq[string]
proc newHttpHeaders*(): HttpHeaders =
new result
result.table = newTable[string, seq[string]]()
proc newHttpHeaders*(keyValuePairs:
seq[tuple[key: string, val: string]]): HttpHeaders =
var pairs: seq[tuple[key: string, val: seq[string]]] = @[]
for pair in keyValuePairs:
pairs.add((pair.key.toLowerAscii(), @[pair.val]))
new result
result.table = newTable[string, seq[string]](pairs)
proc `$`*(headers: HttpHeaders): string =
return $headers.table
proc clear*(headers: HttpHeaders) =
headers.table.clear()
proc `[]`*(headers: HttpHeaders, key: string): HttpHeaderValues =
## Returns the values associated with the given ``key``. If the returned
## values are passed to a procedure expecting a ``string``, the first
## value is automatically picked. If there are
## no values associated with the key, an exception is raised.
##
## To access multiple values of a key, use the overloaded ``[]`` below or
## to get all of them access the ``table`` field directly.
return headers.table[key.toLowerAscii].HttpHeaderValues
# converter toString*(values: HttpHeaderValues): string =
# return seq[string](values)[0]
proc `[]`*(headers: HttpHeaders, key: string, i: int): string =
## Returns the ``i``'th value associated with the given key. If there are
## no values associated with the key or the ``i``'th value doesn't exist,
## an exception is raised.
return headers.table[key.toLowerAscii][i]
proc `[]=`*(headers: HttpHeaders, key, value: string) =
## Sets the header entries associated with ``key`` to the specified value.
## Replaces any existing values.
headers.table[key.toLowerAscii] = @[value]
proc `[]=`*(headers: HttpHeaders, key: string, value: seq[string]) =
## Sets the header entries associated with ``key`` to the specified list of
## values.
## Replaces any existing values.
headers.table[key.toLowerAscii] = value
proc add*(headers: HttpHeaders, key, value: string) =
## Adds the specified value to the specified key. Appends to any existing
## values associated with the key.
if not headers.table.hasKey(key.toLowerAscii):
headers.table[key.toLowerAscii] = @[value]
else:
headers.table[key.toLowerAscii].add(value)
proc del*(headers: HttpHeaders, key: string) =
## Delete the header entries associated with ``key``
headers.table.del(key.toLowerAscii)
iterator pairs*(headers: HttpHeaders): tuple[key, value: string] =
## Yields each key, value pair.
for k, v in headers.table:
for value in v:
yield (k, value)
proc contains*(values: HttpHeaderValues, value: string): bool =
## Determines if ``value`` is one of the values inside ``values``. Comparison
## is performed without case sensitivity.
for val in seq[string](values):
if val.toLowerAscii == value.toLowerAscii: return true
proc hasKey*(headers: HttpHeaders, key: string): bool =
return headers.table.hasKey(key.toLowerAscii())
proc getOrDefault*(headers: HttpHeaders, key: string,
default = @[""].HttpHeaderValues): HttpHeaderValues =
## Returns the values associated with the given ``key``. If there are no
## values associated with the key, then ``default`` is returned.
if headers.hasKey(key):
return headers[key]
else:
return default
proc len*(headers: HttpHeaders): int = return headers.table.len
type SameSite* = enum
None, Strict, Lax
proc buildSetCookieHeader*(cookiename, cookievalue: string, domain="", expires="", maxage=0, path="", sameSite=None, secure=false, httponly=false): string =
var validSeq:seq[string] = @[]
result = "Set-Cookie: "
result &= fmt"{cookiename}={cookievalue}"
if expires.len > 0:
validSeq.add(fmt"Expires={expires}")
if domain.len > 0:
validSeq.add(fmt"Domain={domain}")
if maxage > 0:
validSeq.add(fmt"Max-Age={maxage}")
if path.len > 0:
validSeq.add(fmt"Path={path}")
if secure == true:
validSeq.add(fmt"Secure")
if httponly == true:
validSeq.add(fmt"HttpOnly")
case sameSite
of Strict: validSeq.add("SameSite=Strict")
of Lax: validSeq.add("SameSite=Lax")
else: discard
result &= validSeq.join("; ")
# Set-Cookie: <cookie-name>=<cookie-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
# Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
# Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Secure
# Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None
# // Multiple directives are also possible, for example:
# Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
proc httpMethodFromString(txt: string): Option[HttpMethod] =
let s2m = {"GET": HttpGet, "POST": HttpPost, "PUT":HttpPut, "PATCH": HttpPatch, "DELETE": HttpDelete, "HEAD":HttpHead}.toTable
if txt in s2m:
result = some(s2m[txt.toUpper])
else:
result = none(HttpMethod)
proc parseList(line: string, list: var seq[string], start: int, sep=','): int =
var i = 0
var current = ""
while start+i < line.len and line[start + i] notin {'\c', '\l'}:
i += line.skipWhitespace(start + i)
i += line.parseUntil(current, {'\c', '\l', sep}, start + i)
list.add(current)
if start+i < line.len and line[start + i] == sep:
i.inc # Skip ,
current.setLen(0)
proc parseHeader*(line: string, sep=','): tuple[key: string, value: seq[string]] =
## Parses a single raw header HTTP line into key value pairs.
##
## Used by ``asynchttpserver`` and ``httpclient`` internally and should not
## be used by you.
result.value = @[]
var i = 0
i = line.parseUntil(result.key, ':')
inc(i) # skip :
if i < len(line):
i += parseList(line, result.value, i, sep=sep)
elif result.key.len > 0:
result.value = @[""]
else:
result.value = @[]
const maxLine = 8*1024
type Request* = object
httpMethod*: HTTPMethod
requestURI*: string
httpVersion*: HttpVersion
headers*: HTTPHeaders
path*: string
body*: string
raw_body: string
queryParams*: TableRef[string, string]
formData*: TableRef[string, string]
urlParams*: TableRef[string, string]
cookies*: TableRef[string, string]
type Response* = object
headers: HttpHeaders
httpver: HttpVersion
code: HttpCode
content: string
proc newResponse*(): ref Response =
new result
result.httpver = HttpVer11
result.headers = newHttpHeaders()
type MiddlewareFunc* = proc(req: var Request): (ref Response, bool) {.nimcall.}
type HandlerFunc* = proc(req: var Request):ref Response {.nimcall.}
type RouterValue* = object
handlerFunc: HandlerFunc
httpMethod: HttpMethod
middlewares:seq[MiddlewareFunc]
type Router* = object
table: TableRef[string, RouterValue]
notFoundHandler: HandlerFunc
proc abortWith*(msg: string): ref Response =
result = newResponse()
result.code = Http404
result.content = msg
proc redirectTo*(url: string, code=Http301): ref Response =
result = newResponse()
result.code = code
result.headers.add("Location", url)
proc handle404*(req: var Request): ref Response =
var resp = newResponse()
resp.code = Http404
resp.content = fmt"nothing at {req.path}"
return resp
proc newRouter*(notFoundHandler:HandlerFunc=handle404): ref Router =
result = new Router
result.table = newTable[string, RouterValue]()
result.notFoundHandler = notFoundHandler
iterator registeredRoutes*(r: ref Router): (string, string) =
for pat, routerValue in r.table:
yield (pat, $routerValue.httpMethod)
proc printRegisteredRoutes*(r: ref Router) =
let t = newUnicodeTable()
t.setHeaders(@[newCell("Method", pad=5), newCell("Route", rightpad=10)])
for pat, meth in r.registeredRoutes:
t.addRow(@[meth, pat])
printTable(t)
proc getByPath*(r: ref Router, path: string, httpMethod=HttpGet) : (RouterValue, TableRef[string, string]) =
var found = false
if path in r.table: # exact match
return (r.table[path], newTable[string, string]())
for handlerPath, routerValue in r.table.pairs:
if routerValue.httpMethod != httpMethod:
continue
echo fmt"checking handler: {handlerPath} if it matches {path}"
let pathParts = path.split({'/'})
let handlerPathParts = handlerPath.split({'/'})
echo fmt"pathParts {pathParts} and handlerPathParts {handlerPathParts}"
if len(pathParts) != len(handlerPathParts):
echo "length isn't ok"
continue
else:
var idx = 0
var capturedParams = newTable[string, string]()
while idx<len(pathParts):
let pathPart = pathParts[idx]
let handlerPathPart = handlerPathParts[idx]
echo fmt"current pathPart {pathPart} current handlerPathPart: {handlerPathPart}"
if handlerPathPart.startsWith(":") or handlerPathPart.startsWith("@"):
echo fmt"found var in path {handlerPathPart} matches {pathPart}"
capturedParams[handlerPathPart[1..^1]] = pathPart
inc idx
else:
if pathPart == handlerPathPart:
inc idx
else:
break
if idx == len(pathParts):
found = true
return (routerValue, capturedParams)
if not found:
return (RouterValue(handlerFunc:r.notFoundHandler, middlewares: @[]), newTable[string, string]())
proc addRoute*(router: ref Router, route: string, handler: HandlerFunc, httpMethod:HttpMethod=HttpGet, middlewares:seq[MiddlewareFunc]= @[]) =
router.table.add(route, RouterValue(handlerFunc:handler, httpMethod: httpMethod, middlewares:middlewares))
let addHandler = addRoute
type ServerOptions* = object
address: string
port: Port
type Servy = object
options: ServerOptions
router: ref Router
middlewares: seq[MiddlewareFunc]
staticDir: string
sock: AsyncSocket
proc parseQueryParams(content: string): TableRef[string, string] =
## BUG IN JESTER.
result = newTable[string, string]()
var consumed = 0
if "?" notin content and "=" notin content:
return
if "?" in content:
consumed += content.skipUntil({'?'}, consumed)
inc consumed # skip ? now.
while consumed < content.len:
if "=" notin content[consumed..^1]:
break
var key = ""
var val = ""
consumed += content.parseUntil(key, "=", consumed)
inc consumed # =
consumed += content.parseUntil(val, "&", consumed)
inc consumed
# result[decodeUrl(key)] = result[decodeUrl(val)]
result.add(decodeUrl(key), decodeUrl(val))
echo "consumed:" & $consumed
echo "contentlen:" & $content.len
type FormPart = object
name*: string
headers*: HttpHeaders
body*: string
proc newFormPart(): ref FormPart =
new result
result.headers = newHttpHeaders()
proc `$`(this:ref FormPart): string =
result = fmt"partname: {this.name} partheaders: {this.headers} partbody: {this.body}"
type FormMultiPart = object
parts*: TableRef[string, ref FormPart]
proc newFormMultiPart(): ref FormMultiPart =
new result
result.parts = newTable[string, ref FormPart]()
proc `$`(this: ref FormMultiPart): string =
return fmt"parts: {this.parts}"
proc parseFormData(r: Request): ref FormMultiPart =
discard """
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["241"], "content-type": @["multipart/form-data; boundary=------------------------95909933ebe184f2"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "--------------------------95909933ebe184f2\c\nContent-Disposition: form-data; name=\"who\"\c\n\c\nhamada\c\n--------------------------95909933ebe184f2\c\nContent-Disposition: form-data; name=\"next\"\c\n\c\nhome\c\n--------------------------95909933ebe184f2--\c\n", raw_body: "", queryParams: {:})
"""
result = newFormMultiPart()
let contenttype = r.headers.getOrDefault("content-type")[0]
let body = r.body
if "form-urlencoded" in contenttype.toLowerAscii():
# query params are the post body
let postBodyAsParams = parseQueryParams(body)
for k, v in postBodyAsParams.pairs:
r.queryParams.add(k, v)
elif contenttype.startsWith("multipart/") and "boundary" in contenttype:
var boundaryName = contenttype[contenttype.find("boundary=")+"boundary=".len..^1]
echo "boundayName: " & boundaryName
for partString in body.split(boundaryName & "\c\L"):
var part = newFormPart()
var partName = ""
var totalParsedLines = 1
let bodyLines = body.split("\c\L")[1..^1] # at the boundary line
for line in bodyLines:
if line.strip().len != 0:
let splitted = line.split(": ")
if len(splitted) == 2:
part.headers.add(splitted[0], splitted[1])
elif len(splitted) == 1:
part.headers.add(splitted[0], "")
if "content-disposition" in line.toLowerAscii and "name" in line.toLowerAscii:
# Content-Disposition: form-data; name="next"
var consumed = line.find("name=")+"name=".len
discard line.skip("\"", consumed)
inc consumed
consumed += line.parseUntil(partName, "\"", consumed)
else:
break # done with headers now for the body.
inc totalParsedLines
let content = join(bodyLines[totalParsedLines..^1], "\c\L")
part.body = content
part.name = partName
result.parts.add(partName, part)
echo $result.parts
proc parseRequestFromConnection(s: ref Servy, conn:AsyncSocket): Future[Request] {.async.} =
let requestline = $await conn.recvLine(maxLength=maxLine)
var meth, path, httpver: string
var parts = requestLine.splitWhitespace()
meth = parts[0]
path = parts[1]
httpver = parts[2]
var contentLength = 0
echo meth, path, httpver
let m = httpMethodFromString(meth)
if m.isSome:
result.httpMethod = m.get()
else:
echo meth
raise newException(OSError, "invalid httpmethod")
if "1.1" in httpver:
result.httpVersion = HttpVer11
elif "1.0" in httpver:
result.httpVersion = HttpVer10
result.path = path
result.headers = newHttpHeaders()
result.queryParams = newTable[string, string]()
result.cookies = newTable[string, string]()
result.formData = newTable[string, string]()
if "?" in path:
# has query params
result.queryParams = parseQueryParams(path)
# parse headers
var line = ""
line = $(await conn.recvLine(maxLength=maxLine))
echo fmt"line: >{line}< "
while line != "\r\n":
var kv: tuple[key: string, value: seq[string]]
# a header line
if line.toLowerAscii.startsWith("cookie"):
kv = parseHeader(line, sep=';')
else:
kv = parseHeader(line)
result.headers[kv.key] = kv.value
if kv.key.toLowerAscii == "content-length":
contentLength = parseInt(kv.value[0])
if kv.key.toLowerAscii == "cookie":
for cookieinfo in kv.value:
let theparts = cookieinfo.split({'='})
let cookiename = theparts[0]
let cookieval = theparts[1]
result.cookies[cookiename] = cookieval
line = $(await conn.recvLine(maxLength=maxLine))
# echo fmt"line: >{line}< "
if contentLength > 0:
result.body = await conn.recv(contentLength)
# FIXME: remember to add raw_body later
# echo "ok body is : " & result.body
result.urlParams = newTable[string, string]()
discard result.parseFormData()
proc parseRequestString(input: string): Request =
let lines = input.splitLines()
echo lines
let requestLine = lines[0]
var meth, path, httpver: string
var parts = requestLine.splitWhitespace()
meth = parts[0]
path = parts[1]
httpver = parts[2]
var contentLength = 0
echo meth, path, httpver
let m = httpMethodFromString(meth)
if m.isSome:
result.httpMethod = m.get()
else:
echo meth
raise newException(OSError, "invalid httpmethod")
if "1.1" in httpver:
result.httpVersion = HttpVer11
elif "1.0" in httpver:
result.httpVersion = HttpVer10
result.path = path
result.headers = newHttpHeaders()
# parse headers
var curLineIdx = 1
while curLineIdx<lines.len and lines[curLineIdx] != "\r\n":
# a header line
let kv = parseHeader(lines[curLineIdx])
result.headers[kv.key] = kv.value
if kv.key.toLowerAscii == "content-length":
contentLength = parseInt(kv.value[0])
inc(curLineIdx)
if contentLength>0:
let remainingContent = join(lines[curLineIdx..^1], "\r\n")
echo "remaining.. " & remainingContent
let content = remainingContent[0..contentLength]
echo "ok body is : " & content
proc `$`(ver:HttpVersion): string =
case ver
of HttpVer10: result="HTTP/1.0"
of HttpVer11: result="HTTP/1.1"
proc `$`(m:HttpMethod): string =
case m
of HttpHead: result="HEAD"
of HttpGet: result= "GET"
of HttpPost: result="POST"
of HttpPut: result="PUT"
of HttpDelete: result="DELETE"
of HttpTrace: result="TRACE"
of HttpOptions: result="OPTIONS"
of HttpConnect: result="CONNECT"
of HttpPatch: result="PATCH"
proc formatStatusLine(code: HttpCode, httpver: HttpVersion) : string =
return fmt"{httpver} {code}" & "\r\n"
proc formatResponse(code:HttpCode, httpver:HttpVersion, content:string, headers:HttpHeaders): string =
result &= formatStatusLine(code, httpver)
if headers.len > 0:
for k,v in headers.pairs:
result &= fmt"{k}: {v}" & "\r\n"
result &= fmt"Content-Length: {content.len}" & "\r\n\r\n"
result &= content
echo "will send"
echo result
proc format*(resp: ref Response) : string =
result = formatResponse(resp.code, resp.httpver, resp.content, resp.headers)
proc newServy*(options: ServerOptions, router:ref Router, middlewares:seq[MiddlewareFunc]): ref Servy =
result = new Servy
result.options = options
result.router = router
result.middlewares = middlewares
result.sock = newAsyncSocket()
result.sock.setSockOpt(OptReuseAddr, true)
proc handleClient*(s: ref Servy, client: AsyncSocket) {.async.} =
var req = await s.parseRequestFromConnection(client)
for m in s.middlewares:
let (resp, usenextmiddleware) = m(req)
if not usenextmiddleware:
echo "early return from middleware..."
await client.send(resp.format())
return
echo "received request from client: " & $req
let (routeHandler, params) = s.router.getByPath(req.path)
req.urlParams = params
let handler = routeHandler.handlerFunc
let middlewares = routeHandler.middlewares
for m in middlewares:
let (resp, usenextmiddleware) = m(req)
if not usenextmiddleware:
echo "early return from route middleware..."
await client.send(resp.format())
return
let resp = handler(req)
echo "reached the handler safely.. and executing now."
await client.send(resp.format())
echo $req.formData
proc serve*(s: ref Servy) {.async.} =
s.sock.bindAddr(s.options.port)
s.sock.listen()
s.router.printRegisteredRoutes
while true:
let client = await s.sock.accept()
asyncCheck s.handleClient(client)
runForever()
proc newStaticMiddleware(dir: string): proc(request: var Request): (ref Response, bool) =
result = proc(request: var Request): (ref Response, bool) =
let path = request.path
echo path
return (newResponse(), false)
when isMainModule:
const sampleRequest = """GET /index.html HTTP/1.1
Host: localhost
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
"""
echo sampleRequest
discard """
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["241"], "content-type": @["multipart/form-data; boundary=------------------------94f28cb187c245d8"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "--------------------------94f28cb187c245d8\c\nContent-Disposition: form-data; name=\"who\"\c\n\c\nhamada\c\n--------------------------94f28cb187c245d8\c\nContent-Disposition: form-data; name=\"next\"\c\n\c\nhome\c\n--------------------------94f28cb187c245d8--\c\n", raw_body: "")
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["44"], "content-type": @["application/x-www-form-urlencoded"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "{\"username\":\"ahmed\", \"password\":\"apassword\"}", raw_body: "")
"""
# echo $parseRequestString(sampleRequest)
# echo("Hello, World!")
proc main() =
var router = newRouter()
proc handleHello(req:var Request): ref Response =
result = newResponse()
result.code = Http200
result.content = "hello world from handler /hello" & $req
let loggingMiddleware = proc(request: var Request): (ref Response, bool) =
let path = request.path
let headers = request.headers
echo "==============================="
echo "from logger handler"
echo "path: " & path
echo "headers: " & $headers
echo "==============================="
return (newResponse(), true)
let trimTrailingSlash = proc(request: var Request): (ref Response, bool) =
let path = request.path
if path.endswith("/"):
request.path = path[0..^2]
echo "==============================="
echo "from slash trimmer "
echo "path was : " & path
echo "path: " & request.path
echo "==============================="
return (newResponse(), true)
router.addRoute("/hello", handleHello)
let assertJwtFieldExists = proc(request: var Request): (ref Response, bool) =
echo $request.headers
let jwtHeaderVals = request.headers.getOrDefault("jwt", @[""])
let jwt = jwtHeaderVals[0]
echo "================\n\njwt middleware"
if jwt.len != 0:
echo fmt"bye bye {jwt} "
else:
echo fmt"sure bye but i didn't get ur name"
echo "===================\n\n"
return (newResponse(), true)
router.addRoute("/bye", handleHello, HttpGet, @[assertJwtFieldExists])
proc handleGreet(req:var Request): ref Response =
result = newResponse()
result.code = Http200
result.content = "generic greet" & $req
router.addRoute("/greet", handleGreet, HttpGet, @[])
router.addRoute("/greet/:username", handleGreet, HttpGet, @[])
router.addRoute("/greet/:first/:second/:lang", handleGreet, HttpGet, @[])
proc handleAbort(req:var Request): ref Response =
result = abortWith("sorry mate")
proc handleRedirect(req:var Request): ref Response =
result = redirectTo("https://python.org")
router.addRoute("/redirect", handleRedirect, HttpGet)
router.addRoute("/abort", handleAbort, HttpGet)
let serveTmpDir = newStaticMiddleware("/tmp")
let opts = ServerOptions(address:"127.0.0.1", port:9000.Port)
var s = newServy(opts, router, @[loggingMiddleware, trimTrailingSlash, serveTmpDir])
asyncCheck s.serve()
echo "servy started..."
runForever()
main()
# This is just an example to get you started. A typical binary package
# uses this file as the main entry point of the application.
import strformat, tables, json, strutils, asyncdispatch, asyncnet, strutils, parseutils, options, net
from cgi import decodeUrl
import terminaltables
import mimetypes
type
HttpVersion* = enum
HttpVer11,
HttpVer10
HttpMethod* = enum ## the requested HttpMethod
HttpHead, ## Asks for the response identical to the one that would
## correspond to a GET request, but without the response
## body.
HttpGet, ## Retrieves the specified resource.
HttpPost, ## Submits data to be processed to the identified
## resource. The data is included in the body of the
## request.
HttpPut, ## Uploads a representation of the specified resource.
HttpDelete, ## Deletes the specified resource.
HttpTrace, ## Echoes back the received request, so that a client
## can see what intermediate servers are adding or
## changing in the request.
HttpOptions, ## Returns the HTTP methods that the server supports
## for specified address.
HttpConnect, ## Converts the request connection to a transparent
## TCP/IP tunnel, usually used for proxies.
HttpPatch ## Applies partial modifications to a resource.
HttpCode* = distinct range[0 .. 599]
const
Http100* = HttpCode(100)
Http101* = HttpCode(101)
Http200* = HttpCode(200)
Http201* = HttpCode(201)
Http202* = HttpCode(202)
Http203* = HttpCode(203)
Http204* = HttpCode(204)
Http205* = HttpCode(205)
Http206* = HttpCode(206)
Http300* = HttpCode(300)
Http301* = HttpCode(301)
Http302* = HttpCode(302)
Http303* = HttpCode(303)
Http304* = HttpCode(304)
Http305* = HttpCode(305)
Http307* = HttpCode(307)
Http400* = HttpCode(400)
Http401* = HttpCode(401)
Http403* = HttpCode(403)
Http404* = HttpCode(404)
Http405* = HttpCode(405)
Http406* = HttpCode(406)
Http407* = HttpCode(407)
Http408* = HttpCode(408)
Http409* = HttpCode(409)
Http410* = HttpCode(410)
Http411* = HttpCode(411)
Http412* = HttpCode(412)
Http413* = HttpCode(413)
Http414* = HttpCode(414)
Http415* = HttpCode(415)
Http416* = HttpCode(416)
Http417* = HttpCode(417)
Http418* = HttpCode(418)
Http421* = HttpCode(421)
Http422* = HttpCode(422)
Http426* = HttpCode(426)
Http428* = HttpCode(428)
Http429* = HttpCode(429)
Http431* = HttpCode(431)
Http451* = HttpCode(451)
Http500* = HttpCode(500)
Http501* = HttpCode(501)
Http502* = HttpCode(502)
Http503* = HttpCode(503)
Http504* = HttpCode(504)
Http505* = HttpCode(505)
proc `$`*(code: HttpCode): string =
## Converts the specified ``HttpCode`` into a HTTP status.
##
## For example:
##
## .. code-block:: nim
## doAssert($Http404 == "404 Not Found")
case code.int
of 100: "100 Continue"
of 101: "101 Switching Protocols"
of 200: "200 OK"
of 201: "201 Created"
of 202: "202 Accepted"
of 203: "203 Non-Authoritative Information"
of 204: "204 No Content"
of 205: "205 Reset Content"
of 206: "206 Partial Content"
of 300: "300 Multiple Choices"
of 301: "301 Moved Permanently"
of 302: "302 Found"
of 303: "303 See Other"
of 304: "304 Not Modified"
of 305: "305 Use Proxy"
of 307: "307 Temporary Redirect"
of 400: "400 Bad Request"
of 401: "401 Unauthorized"
of 403: "403 Forbidden"
of 404: "404 Not Found"
of 405: "405 Method Not Allowed"
of 406: "406 Not Acceptable"
of 407: "407 Proxy Authentication Required"
of 408: "408 Request Timeout"
of 409: "409 Conflict"
of 410: "410 Gone"
of 411: "411 Length Required"
of 412: "412 Precondition Failed"
of 413: "413 Request Entity Too Large"
of 414: "414 Request-URI Too Long"
of 415: "415 Unsupported Media Type"
of 416: "416 Requested Range Not Satisfiable"
of 417: "417 Expectation Failed"
of 418: "418 I'm a teapot"
of 421: "421 Misdirected Request"
of 422: "422 Unprocessable Entity"
of 426: "426 Upgrade Required"
of 428: "428 Precondition Required"
of 429: "429 Too Many Requests"
of 431: "431 Request Header Fields Too Large"
of 451: "451 Unavailable For Legal Reasons"
of 500: "500 Internal Server Error"
of 501: "501 Not Implemented"
of 502: "502 Bad Gateway"
of 503: "503 Service Unavailable"
of 504: "504 Gateway Timeout"
of 505: "505 HTTP Version Not Supported"
of 506: "506 Variant Also Negotiates"
of 507: "507 Insufficient Storage"
of 508: "508 Loop Detected"
of 510: "510 Not Extended"
of 511: "511 Network Authentication Required"
of 599: "599 Network Connect Timeout Error"
else: $(int(code))
const headerLimit* = 10_000
type HttpHeaders* = ref object
table*: TableRef[string, seq[string]]
type HttpHeaderValues* = seq[string]
proc newHttpHeaders*(): HttpHeaders =
new result
result.table = newTable[string, seq[string]]()
proc newHttpHeaders*(keyValuePairs:
seq[tuple[key: string, val: string]]): HttpHeaders =
var pairs: seq[tuple[key: string, val: seq[string]]] = @[]
for pair in keyValuePairs:
pairs.add((pair.key.toLowerAscii(), @[pair.val]))
new result
result.table = newTable[string, seq[string]](pairs)
proc `$`*(headers: HttpHeaders): string =
return $headers.table
proc clear*(headers: HttpHeaders) =
headers.table.clear()
proc `[]`*(headers: HttpHeaders, key: string): HttpHeaderValues =
## Returns the values associated with the given ``key``. If the returned
## values are passed to a procedure expecting a ``string``, the first
## value is automatically picked. If there are
## no values associated with the key, an exception is raised.
##
## To access multiple values of a key, use the overloaded ``[]`` below or
## to get all of them access the ``table`` field directly.
return headers.table[key.toLowerAscii].HttpHeaderValues
# converter toString*(values: HttpHeaderValues): string =
# return seq[string](values)[0]
proc `[]`*(headers: HttpHeaders, key: string, i: int): string =
## Returns the ``i``'th value associated with the given key. If there are
## no values associated with the key or the ``i``'th value doesn't exist,
## an exception is raised.
return headers.table[key.toLowerAscii][i]
proc `[]=`*(headers: HttpHeaders, key, value: string) =
## Sets the header entries associated with ``key`` to the specified value.
## Replaces any existing values.
headers.table[key.toLowerAscii] = @[value]
proc `[]=`*(headers: HttpHeaders, key: string, value: seq[string]) =
## Sets the header entries associated with ``key`` to the specified list of
## values.
## Replaces any existing values.
headers.table[key.toLowerAscii] = value
proc add*(headers: HttpHeaders, key, value: string) =
## Adds the specified value to the specified key. Appends to any existing
## values associated with the key.
if not headers.table.hasKey(key.toLowerAscii):
headers.table[key.toLowerAscii] = @[value]
else:
headers.table[key.toLowerAscii].add(value)
proc del*(headers: HttpHeaders, key: string) =
## Delete the header entries associated with ``key``
headers.table.del(key.toLowerAscii)
iterator pairs*(headers: HttpHeaders): tuple[key, value: string] =
## Yields each key, value pair.
for k, v in headers.table:
for value in v:
yield (k, value)
proc contains*(values: HttpHeaderValues, value: string): bool =
## Determines if ``value`` is one of the values inside ``values``. Comparison
## is performed without case sensitivity.
for val in seq[string](values):
if val.toLowerAscii == value.toLowerAscii: return true
proc hasKey*(headers: HttpHeaders, key: string): bool =
return headers.table.hasKey(key.toLowerAscii())
proc getOrDefault*(headers: HttpHeaders, key: string,
default = @[""].HttpHeaderValues): HttpHeaderValues =
## Returns the values associated with the given ``key``. If there are no
## values associated with the key, then ``default`` is returned.
if headers.hasKey(key):
return headers[key]
else:
return default
proc len*(headers: HttpHeaders): int = return headers.table.len
type SameSite* = enum
None, Strict, Lax
proc buildSetCookieHeader*(cookiename, cookievalue: string, domain="", expires="", maxage=0, path="", sameSite=None, secure=false, httponly=false): string =
var validSeq:seq[string] = @[]
result = "Set-Cookie: "
result &= fmt"{cookiename}={cookievalue}"
if expires.len > 0:
validSeq.add(fmt"Expires={expires}")
if domain.len > 0:
validSeq.add(fmt"Domain={domain}")
if maxage > 0:
validSeq.add(fmt"Max-Age={maxage}")
if path.len > 0:
validSeq.add(fmt"Path={path}")
if secure == true:
validSeq.add(fmt"Secure")
if httponly == true:
validSeq.add(fmt"HttpOnly")
case sameSite
of Strict: validSeq.add("SameSite=Strict")
of Lax: validSeq.add("SameSite=Lax")
else: discard
result &= validSeq.join("; ")
# Set-Cookie: <cookie-name>=<cookie-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
# Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
# Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
# Set-Cookie: <cookie-name>=<cookie-value>; Secure
# Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
# Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None
# // Multiple directives are also possible, for example:
# Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
proc httpMethodFromString(txt: string): Option[HttpMethod] =
let s2m = {"GET": HttpGet, "POST": HttpPost, "PUT":HttpPut, "PATCH": HttpPatch, "DELETE": HttpDelete, "HEAD":HttpHead}.toTable
if txt in s2m:
result = some(s2m[txt.toUpper])
else:
result = none(HttpMethod)
proc parseList(line: string, list: var seq[string], start: int, sep=','): int =
var i = 0
var current = ""
while start+i < line.len and line[start + i] notin {'\c', '\l'}:
i += line.skipWhitespace(start + i)
i += line.parseUntil(current, {'\c', '\l', sep}, start + i)
list.add(current)
if start+i < line.len and line[start + i] == sep:
i.inc # Skip ,
current.setLen(0)
proc parseHeader*(line: string, sep=','): tuple[key: string, value: seq[string]] =
## Parses a single raw header HTTP line into key value pairs.
##
## Used by ``asynchttpserver`` and ``httpclient`` internally and should not
## be used by you.
result.value = @[]
var i = 0
i = line.parseUntil(result.key, ':')
inc(i) # skip :
if i < len(line):
i += parseList(line, result.value, i, sep=sep)
elif result.key.len > 0:
result.value = @[""]
else:
result.value = @[]
const maxLine = 8*1024
type Request* = object
httpMethod*: HTTPMethod
requestURI*: string
httpVersion*: HttpVersion
headers*: HTTPHeaders
path*: string
body*: string
raw_body: string
queryParams*: TableRef[string, string]
formData*: TableRef[string, string]
urlParams*: TableRef[string, string]
cookies*: TableRef[string, string]
type Response* = object
headers: HttpHeaders
httpver: HttpVersion
code: HttpCode
content: string
proc newResponse*(): ref Response =
new result
result.httpver = HttpVer11
result.headers = newHttpHeaders()
type MiddlewareFunc* = proc(req: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.}
type HandlerFunc* = proc(req: var Request):ref Response {.nimcall.}
type RouterValue* = object
handlerFunc: HandlerFunc
httpMethod: HttpMethod
middlewares:seq[MiddlewareFunc]
type Router* = object
table: TableRef[string, RouterValue]
notFoundHandler: HandlerFunc
proc abortWith*(msg: string): ref Response =
result = newResponse()
result.code = Http404
result.content = msg
proc redirectTo*(url: string, code=Http301): ref Response =
result = newResponse()
result.code = code
result.headers.add("Location", url)
proc handle404*(req: var Request): ref Response =
var resp = newResponse()
resp.code = Http404
resp.content = fmt"nothing at {req.path}"
return resp
proc newRouter*(notFoundHandler:HandlerFunc=handle404): ref Router =
result = new Router
result.table = newTable[string, RouterValue]()
result.notFoundHandler = notFoundHandler
iterator registeredRoutes*(r: ref Router): (string, string) =
for pat, routerValue in r.table:
yield (pat, $routerValue.httpMethod)
proc printRegisteredRoutes*(r: ref Router) =
let t = newUnicodeTable()
t.setHeaders(@[newCell("Method", pad=5), newCell("Route", rightpad=10)])
for pat, meth in r.registeredRoutes:
t.addRow(@[meth, pat])
printTable(t)
proc getByPath*(r: ref Router, path: string, httpMethod=HttpGet) : (RouterValue, TableRef[string, string]) =
var found = false
if path in r.table: # exact match
return (r.table[path], newTable[string, string]())
for handlerPath, routerValue in r.table.pairs:
if routerValue.httpMethod != httpMethod:
continue
echo fmt"checking handler: {handlerPath} if it matches {path}"
let pathParts = path.split({'/'})
let handlerPathParts = handlerPath.split({'/'})
echo fmt"pathParts {pathParts} and handlerPathParts {handlerPathParts}"
if len(pathParts) != len(handlerPathParts):
echo "length isn't ok"
continue
else:
var idx = 0
var capturedParams = newTable[string, string]()
while idx<len(pathParts):
let pathPart = pathParts[idx]
let handlerPathPart = handlerPathParts[idx]
echo fmt"current pathPart {pathPart} current handlerPathPart: {handlerPathPart}"
if handlerPathPart.startsWith(":") or handlerPathPart.startsWith("@"):
echo fmt"found var in path {handlerPathPart} matches {pathPart}"
capturedParams[handlerPathPart[1..^1]] = pathPart
inc idx
else:
if pathPart == handlerPathPart:
inc idx
else:
break
if idx == len(pathParts):
found = true
return (routerValue, capturedParams)
if not found:
return (RouterValue(handlerFunc:r.notFoundHandler, middlewares: @[]), newTable[string, string]())
proc addRoute*(router: ref Router, route: string, handler: HandlerFunc, httpMethod:HttpMethod=HttpGet, middlewares:seq[MiddlewareFunc]= @[]) =
router.table.add(route, RouterValue(handlerFunc:handler, httpMethod: httpMethod, middlewares:middlewares))
let addHandler = addRoute
type ServerOptions* = object
address: string
port: Port
type Servy = object
options: ServerOptions
router: ref Router
middlewares: seq[MiddlewareFunc]
staticDir: string
sock: AsyncSocket
proc parseQueryParams(content: string): TableRef[string, string] =
## BUG IN JESTER.
result = newTable[string, string]()
var consumed = 0
if "?" notin content and "=" notin content:
return
if "?" in content:
consumed += content.skipUntil({'?'}, consumed)
inc consumed # skip ? now.
while consumed < content.len:
if "=" notin content[consumed..^1]:
break
var key = ""
var val = ""
consumed += content.parseUntil(key, "=", consumed)
inc consumed # =
consumed += content.parseUntil(val, "&", consumed)
inc consumed
# result[decodeUrl(key)] = result[decodeUrl(val)]
result.add(decodeUrl(key), decodeUrl(val))
echo "consumed:" & $consumed
echo "contentlen:" & $content.len
type FormPart = object
name*: string
headers*: HttpHeaders
body*: string
proc newFormPart(): ref FormPart =
new result
result.headers = newHttpHeaders()
proc `$`(this:ref FormPart): string =
result = fmt"partname: {this.name} partheaders: {this.headers} partbody: {this.body}"
type FormMultiPart = object
parts*: TableRef[string, ref FormPart]
proc newFormMultiPart(): ref FormMultiPart =
new result
result.parts = newTable[string, ref FormPart]()
proc `$`(this: ref FormMultiPart): string =
return fmt"parts: {this.parts}"
proc parseFormData(r: Request): ref FormMultiPart =
discard """
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["241"], "content-type": @["multipart/form-data; boundary=------------------------95909933ebe184f2"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "--------------------------95909933ebe184f2\c\nContent-Disposition: form-data; name=\"who\"\c\n\c\nhamada\c\n--------------------------95909933ebe184f2\c\nContent-Disposition: form-data; name=\"next\"\c\n\c\nhome\c\n--------------------------95909933ebe184f2--\c\n", raw_body: "", queryParams: {:})
"""
result = newFormMultiPart()
let contenttype = r.headers.getOrDefault("content-type")[0]
let body = r.body
if "form-urlencoded" in contenttype.toLowerAscii():
# query params are the post body
let postBodyAsParams = parseQueryParams(body)
for k, v in postBodyAsParams.pairs:
r.queryParams.add(k, v)
elif contenttype.startsWith("multipart/") and "boundary" in contenttype:
var boundaryName = contenttype[contenttype.find("boundary=")+"boundary=".len..^1]
echo "boundayName: " & boundaryName
for partString in body.split(boundaryName & "\c\L"):
var part = newFormPart()
var partName = ""
var totalParsedLines = 1
let bodyLines = body.split("\c\L")[1..^1] # at the boundary line
for line in bodyLines:
if line.strip().len != 0:
let splitted = line.split(": ")
if len(splitted) == 2:
part.headers.add(splitted[0], splitted[1])
elif len(splitted) == 1:
part.headers.add(splitted[0], "")
if "content-disposition" in line.toLowerAscii and "name" in line.toLowerAscii:
# Content-Disposition: form-data; name="next"
var consumed = line.find("name=")+"name=".len
discard line.skip("\"", consumed)
inc consumed
consumed += line.parseUntil(partName, "\"", consumed)
else:
break # done with headers now for the body.
inc totalParsedLines
let content = join(bodyLines[totalParsedLines..^1], "\c\L")
part.body = content
part.name = partName
result.parts.add(partName, part)
echo $result.parts
proc parseRequestFromConnection(s: ref Servy, conn:AsyncSocket): Future[Request] {.async.} =
let requestline = $await conn.recvLine(maxLength=maxLine)
var meth, path, httpver: string
var parts = requestLine.splitWhitespace()
meth = parts[0]
path = parts[1]
httpver = parts[2]
var contentLength = 0
echo meth, path, httpver
let m = httpMethodFromString(meth)
if m.isSome:
result.httpMethod = m.get()
else:
echo meth
raise newException(OSError, "invalid httpmethod")
if "1.1" in httpver:
result.httpVersion = HttpVer11
elif "1.0" in httpver:
result.httpVersion = HttpVer10
result.path = path
result.headers = newHttpHeaders()
result.queryParams = newTable[string, string]()
result.cookies = newTable[string, string]()
result.formData = newTable[string, string]()
if "?" in path:
# has query params
result.queryParams = parseQueryParams(path)
# parse headers
var line = ""
line = $(await conn.recvLine(maxLength=maxLine))
echo fmt"line: >{line}< "
while line != "\r\n":
var kv: tuple[key: string, value: seq[string]]
# a header line
if line.toLowerAscii.startsWith("cookie"):
kv = parseHeader(line, sep=';')
else:
kv = parseHeader(line)
result.headers[kv.key] = kv.value
if kv.key.toLowerAscii == "content-length":
contentLength = parseInt(kv.value[0])
if kv.key.toLowerAscii == "cookie":
for cookieinfo in kv.value:
let theparts = cookieinfo.split({'='})
let cookiename = theparts[0]
let cookieval = theparts[1]
result.cookies[cookiename] = cookieval
line = $(await conn.recvLine(maxLength=maxLine))
# echo fmt"line: >{line}< "
if contentLength > 0:
result.body = await conn.recv(contentLength)
# FIXME: remember to add raw_body later
# echo "ok body is : " & result.body
result.urlParams = newTable[string, string]()
discard result.parseFormData()
proc parseRequestString(input: string): Request =
let lines = input.splitLines()
echo lines
let requestLine = lines[0]
var meth, path, httpver: string
var parts = requestLine.splitWhitespace()
meth = parts[0]
path = parts[1]
httpver = parts[2]
var contentLength = 0
echo meth, path, httpver
let m = httpMethodFromString(meth)
if m.isSome:
result.httpMethod = m.get()
else:
echo meth
raise newException(OSError, "invalid httpmethod")
if "1.1" in httpver:
result.httpVersion = HttpVer11
elif "1.0" in httpver:
result.httpVersion = HttpVer10
result.path = path
result.headers = newHttpHeaders()
# parse headers
var curLineIdx = 1
while curLineIdx<lines.len and lines[curLineIdx] != "\r\n":
# a header line
let kv = parseHeader(lines[curLineIdx])
result.headers[kv.key] = kv.value
if kv.key.toLowerAscii == "content-length":
contentLength = parseInt(kv.value[0])
inc(curLineIdx)
if contentLength>0:
let remainingContent = join(lines[curLineIdx..^1], "\r\n")
echo "remaining.. " & remainingContent
let content = remainingContent[0..contentLength]
echo "ok body is : " & content
proc `$`(ver:HttpVersion): string =
case ver
of HttpVer10: result="HTTP/1.0"
of HttpVer11: result="HTTP/1.1"
proc `$`(m:HttpMethod): string =
case m
of HttpHead: result="HEAD"
of HttpGet: result= "GET"
of HttpPost: result="POST"
of HttpPut: result="PUT"
of HttpDelete: result="DELETE"
of HttpTrace: result="TRACE"
of HttpOptions: result="OPTIONS"
of HttpConnect: result="CONNECT"
of HttpPatch: result="PATCH"
proc formatStatusLine(code: HttpCode, httpver: HttpVersion) : string =
return fmt"{httpver} {code}" & "\r\n"
proc formatResponse(code:HttpCode, httpver:HttpVersion, content:string, headers:HttpHeaders): string =
result &= formatStatusLine(code, httpver)
if headers.len > 0:
for k,v in headers.pairs:
result &= fmt"{k}: {v}" & "\r\n"
result &= fmt"Content-Length: {content.len}" & "\r\n\r\n"
result &= content
echo "will send"
echo result
proc format*(resp: ref Response) : string =
result = formatResponse(resp.code, resp.httpver, resp.content, resp.headers)
proc newServy*(options: ServerOptions, router:ref Router, middlewares:seq[MiddlewareFunc]): ref Servy =
result = new Servy
result.options = options
result.router = router
result.middlewares = middlewares
result.sock = newAsyncSocket()
result.sock.setSockOpt(OptReuseAddr, true)
proc handleClient*(s: ref Servy, client: AsyncSocket) {.async.} =
var req = await s.parseRequestFromConnection(client)
for m in s.middlewares:
let (resp, usenextmiddleware) = m(req)
if not usenextmiddleware:
echo "early return from middleware..."
await client.send(resp.format())
return
echo "received request from client: " & $req
let (routeHandler, params) = s.router.getByPath(req.path)
req.urlParams = params
let handler = routeHandler.handlerFunc
let middlewares = routeHandler.middlewares
for m in middlewares:
let (resp, usenextmiddleware) = m(req)
if not usenextmiddleware:
echo "early return from route middleware..."
await client.send(resp.format())
return
let resp = handler(req)
echo "reached the handler safely.. and executing now."
await client.send(resp.format())
echo $req.formData
proc serve*(s: ref Servy) {.async.} =
s.sock.bindAddr(s.options.port)
s.sock.listen()
s.router.printRegisteredRoutes
while true:
let client = await s.sock.accept()
asyncCheck s.handleClient(client)
runForever()
proc newStaticMiddleware(dir: string): proc(request: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.} =
result = proc(request: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.} =
let path = request.path
echo path
return (newResponse(), false)
when isMainModule:
const sampleRequest = """GET /index.html HTTP/1.1
Host: localhost
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
"""
echo sampleRequest
discard """
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["241"], "content-type": @["multipart/form-data; boundary=------------------------94f28cb187c245d8"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "--------------------------94f28cb187c245d8\c\nContent-Disposition: form-data; name=\"who\"\c\n\c\nhamada\c\n--------------------------94f28cb187c245d8\c\nContent-Disposition: form-data; name=\"next\"\c\n\c\nhome\c\n--------------------------94f28cb187c245d8--\c\n", raw_body: "")
received request from client: (httpMethod: HttpPost, requestURI: "", httpVersion: HTTP/1.1, headers: {"accept": @["*/*"], "content-length": @["44"], "content-type": @["application/x-www-form-urlencoded"], "host": @["127.0.0.1:9000"], "user-agent": @["curl/7.62.0-DEV"]}, path: "/post", body: "{\"username\":\"ahmed\", \"password\":\"apassword\"}", raw_body: "")
"""
# echo $parseRequestString(sampleRequest)
# echo("Hello, World!")
proc main() =
var router = newRouter()
proc handleHello(req:var Request): ref Response =
result = newResponse()
result.code = Http200
result.content = "hello world from handler /hello" & $req
let loggingMiddleware = proc(request: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.} =
let path = request.path
let headers = request.headers
echo "==============================="
echo "from logger handler"
echo "path: " & path
echo "headers: " & $headers
echo "==============================="
return (newResponse(), true)
let trimTrailingSlash = proc(request: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.} =
let path = request.path
if path.endswith("/"):
request.path = path[0..^2]
echo "==============================="
echo "from slash trimmer "
echo "path was : " & path
echo "path: " & request.path
echo "==============================="
return (newResponse(), true)
router.addRoute("/hello", handleHello)
let assertJwtFieldExists = proc(request: var Request): (ref Response, bool) {.closure, gcsafe, locks: 0.} =
echo $request.headers
let jwtHeaderVals = request.headers.getOrDefault("jwt", @[""])
let jwt = jwtHeaderVals[0]
echo "================\n\njwt middleware"
if jwt.len != 0:
echo fmt"bye bye {jwt} "
else:
echo fmt"sure bye but i didn't get ur name"
echo "===================\n\n"
return (newResponse(), true)
router.addRoute("/bye", handleHello, HttpGet, @[assertJwtFieldExists])
proc handleGreet(req:var Request): ref Response =
result = newResponse()
result.code = Http200
result.content = "generic greet" & $req
router.addRoute("/greet", handleGreet, HttpGet, @[])
router.addRoute("/greet/:username", handleGreet, HttpGet, @[])
router.addRoute("/greet/:first/:second/:lang", handleGreet, HttpGet, @[])
proc handleAbort(req:var Request): ref Response =
result = abortWith("sorry mate")
proc handleRedirect(req:var Request): ref Response =
result = redirectTo("https://python.org")
router.addRoute("/redirect", handleRedirect, HttpGet)
router.addRoute("/abort", handleAbort, HttpGet)
let serveTmpDir = newStaticMiddleware("/tmp")
let opts = ServerOptions(address:"127.0.0.1", port:9000.Port)
var s = newServy(opts, router, @[loggingMiddleware, trimTrailingSlash, serveTmpDir])
asyncCheck s.serve()
echo "servy started..."
runForever()
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment