195 lines
5.8 KiB
Nim
195 lines
5.8 KiB
Nim
# Copyright (C) 2012 Dominik Picheta
|
|
# MIT License - Look at license.txt for details.
|
|
import parseutils, strtabs, strutils, tables, net, mimetypes, asyncdispatch, os
|
|
from cgi import decodeUrl
|
|
|
|
const
|
|
useHttpBeast* = false # not defined(windows) and not defined(useStdLib)
|
|
|
|
type
|
|
MultiData* = OrderedTable[string, tuple[fields: StringTableRef, body: string]]
|
|
|
|
Settings* = ref object
|
|
staticDir*: string # By default ./public
|
|
appName*: string
|
|
mimes*: MimeDb
|
|
port*: Port
|
|
bindAddr*: string
|
|
reusePort*: bool
|
|
futureErrorHandler*: proc (fut: Future[void]) {.closure, gcsafe.}
|
|
|
|
JesterError* = object of Exception
|
|
|
|
proc parseUrlQuery*(query: string, result: var Table[string, string])
|
|
{.deprecated: "use stdlib".} =
|
|
var i = 0
|
|
i = query.skip("?")
|
|
while i < query.len()-1:
|
|
var key = ""
|
|
var val = ""
|
|
i += query.parseUntil(key, '=', i)
|
|
if query[i] != '=':
|
|
raise newException(ValueError, "Expected '=' at " & $i &
|
|
" but got: " & $query[i])
|
|
inc(i) # Skip =
|
|
i += query.parseUntil(val, '&', i)
|
|
inc(i) # Skip &
|
|
result[decodeUrl(key)] = decodeUrl(val)
|
|
|
|
template parseContentDisposition(): typed =
|
|
var hCount = 0
|
|
while hCount < hValue.len()-1:
|
|
var key = ""
|
|
hCount += hValue.parseUntil(key, {';', '='}, hCount)
|
|
if hValue[hCount] == '=':
|
|
var value = hvalue.captureBetween('"', start = hCount)
|
|
hCount += value.len+2
|
|
inc(hCount) # Skip ;
|
|
hCount += hValue.skipWhitespace(hCount)
|
|
if key == "name": name = value
|
|
newPart[0][key] = value
|
|
else:
|
|
inc(hCount)
|
|
hCount += hValue.skipWhitespace(hCount)
|
|
|
|
proc parseMultiPart*(body: string, boundary: string): MultiData =
|
|
result = initOrderedTable[string, tuple[fields: StringTableRef, body: string]]()
|
|
var mboundary = "--" & boundary
|
|
|
|
var i = 0
|
|
var partsLeft = true
|
|
while partsLeft:
|
|
var firstBoundary = body.skip(mboundary, i)
|
|
if firstBoundary == 0:
|
|
raise newException(ValueError, "Expected boundary. Got: " & body.substr(i, i+25))
|
|
i += firstBoundary
|
|
i += body.skipWhitespace(i)
|
|
|
|
# Headers
|
|
var newPart: tuple[fields: StringTableRef, body: string] = ({:}.newStringTable, "")
|
|
var name = ""
|
|
while true:
|
|
if body[i] == '\c':
|
|
inc(i, 2) # Skip \c\L
|
|
break
|
|
var hName = ""
|
|
i += body.parseUntil(hName, ':', i)
|
|
if body[i] != ':':
|
|
raise newException(ValueError, "Expected : in headers.")
|
|
inc(i) # Skip :
|
|
i += body.skipWhitespace(i)
|
|
var hValue = ""
|
|
i += body.parseUntil(hValue, {'\c', '\L'}, i)
|
|
if toLowerAscii(hName) == "content-disposition":
|
|
parseContentDisposition()
|
|
newPart[0][hName] = hValue
|
|
i += body.skip("\c\L", i) # Skip *one* \c\L
|
|
|
|
# Parse body.
|
|
while true:
|
|
if body[i] == '\c' and body[i+1] == '\L' and
|
|
body.skip(mboundary, i+2) != 0:
|
|
if body.skip("--", i+2+mboundary.len) != 0:
|
|
partsLeft = false
|
|
break
|
|
break
|
|
else:
|
|
newPart[1].add(body[i])
|
|
inc(i)
|
|
i += body.skipWhitespace(i)
|
|
|
|
result.add(name, newPart)
|
|
|
|
proc parseMPFD*(contentType: string, body: string): MultiData =
|
|
var boundaryEqIndex = contentType.find("boundary=")+9
|
|
var boundary = contentType.substr(boundaryEqIndex, contentType.len()-1)
|
|
return parseMultiPart(body, boundary)
|
|
|
|
proc parseCookies*(s: string): Table[string, string] =
|
|
## parses cookies into a string table.
|
|
##
|
|
## The proc is meant to parse the Cookie header set by a client, not the
|
|
## "Set-Cookie" header set by servers.
|
|
|
|
result = initTable[string, string]()
|
|
var i = 0
|
|
while true:
|
|
i += skipWhile(s, {' ', '\t'}, i)
|
|
var keystart = i
|
|
i += skipUntil(s, {'='}, i)
|
|
var keyend = i-1
|
|
if i >= len(s): break
|
|
inc(i) # skip '='
|
|
var valstart = i
|
|
i += skipUntil(s, {';'}, i)
|
|
result[substr(s, keystart, keyend)] = substr(s, valstart, i-1)
|
|
if i >= len(s): break
|
|
inc(i) # skip ';'
|
|
|
|
type
|
|
SameSite* = enum
|
|
None, Lax, Strict
|
|
|
|
proc makeCookie*(key, value, expires: string, domain = "", path = "",
|
|
secure = false, httpOnly = false,
|
|
sameSite = Lax): string =
|
|
result = ""
|
|
result.add key & "=" & value
|
|
if domain != "": result.add("; Domain=" & domain)
|
|
if path != "": result.add("; Path=" & path)
|
|
if expires != "": result.add("; Expires=" & expires)
|
|
if secure: result.add("; Secure")
|
|
if httpOnly: result.add("; HttpOnly")
|
|
if sameSite != None:
|
|
result.add("; SameSite=" & $sameSite)
|
|
|
|
when not declared(tables.getOrDefault):
|
|
template getOrDefault*(tab, key): untyped = tab[key]
|
|
|
|
when not declared(normalizePath) and not declared(normalizedPath):
|
|
proc normalizePath*(path: var string) =
|
|
## Normalize a path.
|
|
##
|
|
## Consecutive directory separators are collapsed, including an initial double slash.
|
|
##
|
|
## On relative paths, double dot (..) sequences are collapsed if possible.
|
|
## On absolute paths they are always collapsed.
|
|
##
|
|
## Warning: URL-encoded and Unicode attempts at directory traversal are not detected.
|
|
## Triple dot is not handled.
|
|
let isAbs = isAbsolute(path)
|
|
var stack: seq[string] = @[]
|
|
for p in split(path, {DirSep}):
|
|
case p
|
|
of "", ".":
|
|
continue
|
|
of "..":
|
|
if stack.len == 0:
|
|
if isAbs:
|
|
discard # collapse all double dots on absoluta paths
|
|
else:
|
|
stack.add(p)
|
|
elif stack[^1] == "..":
|
|
stack.add(p)
|
|
else:
|
|
discard stack.pop()
|
|
else:
|
|
stack.add(p)
|
|
|
|
if isAbs:
|
|
path = DirSep & join(stack, $DirSep)
|
|
elif stack.len > 0:
|
|
path = join(stack, $DirSep)
|
|
else:
|
|
path = "."
|
|
|
|
proc normalizedPath*(path: string): string =
|
|
## Returns a normalized path for the current OS. See `<#normalizePath>`_
|
|
result = path
|
|
normalizePath(result)
|
|
|
|
when false:
|
|
var r = {:}.newStringTable
|
|
parseUrlQuery("FirstName=Mickey", r)
|
|
echo r
|
|
|