# 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