diff options
Diffstat (limited to 'tests/deps/jester-#head')
-rw-r--r-- | tests/deps/jester-#head/jester.nim | 1350 | ||||
-rw-r--r-- | tests/deps/jester-#head/jester.nimble | 22 | ||||
-rw-r--r-- | tests/deps/jester-#head/jester/patterns.nim | 141 | ||||
-rw-r--r-- | tests/deps/jester-#head/jester/private/errorpages.nim | 19 | ||||
-rw-r--r-- | tests/deps/jester-#head/jester/private/utils.nim | 195 | ||||
-rw-r--r-- | tests/deps/jester-#head/jester/request.nim | 184 |
6 files changed, 1911 insertions, 0 deletions
diff --git a/tests/deps/jester-#head/jester.nim b/tests/deps/jester-#head/jester.nim new file mode 100644 index 000000000..013f0d16d --- /dev/null +++ b/tests/deps/jester-#head/jester.nim @@ -0,0 +1,1350 @@ +# Copyright (C) 2015 Dominik Picheta +# MIT License - Look at license.txt for details. +import net, strtabs, re, tables, parseutils, os, strutils, uri, + times, mimetypes, asyncnet, asyncdispatch, macros, md5, + logging, httpcore, asyncfile, macrocache, json, options, + strformat + +import jester/private/[errorpages, utils] +import jester/[request, patterns] + +from cgi import decodeData, decodeUrl, CgiError + +export request +export strtabs +export tables +export httpcore +export MultiData +export HttpMethod +export asyncdispatch + +export SameSite + +when useHttpBeast: + import httpbeast except Settings, Request + import options +else: + import asynchttpserver except Request + +type + MatchProc* = proc (request: Request): Future[ResponseData] {.gcsafe, closure.} + MatchProcSync* = proc (request: Request): ResponseData{.gcsafe, closure.} + + Matcher = object + case async: bool + of false: + syncProc: MatchProcSync + of true: + asyncProc: MatchProc + + ErrorProc* = proc ( + request: Request, error: RouteError + ): Future[ResponseData] {.gcsafe, closure.} + + Jester* = object + when not useHttpBeast: + httpServer*: AsyncHttpServer + settings: Settings + matchers: seq[Matcher] + errorHandlers: seq[ErrorProc] + + MatchType* = enum + MRegex, MSpecial, MStatic + + RawHeaders* = seq[tuple[key, val: string]] + ResponseData* = tuple[ + action: CallbackAction, + code: HttpCode, + headers: Option[RawHeaders], + content: string, + matched: bool + ] + + CallbackAction* = enum + TCActionNothing, TCActionSend, TCActionRaw, TCActionPass + + RouteErrorKind* = enum + RouteException, RouteCode + RouteError* = object + case kind*: RouteErrorKind + of RouteException: + exc: ref Exception + of RouteCode: + data: ResponseData + +const jesterVer = "0.4.0" + +proc toStr(headers: Option[RawHeaders]): string = + return $newHttpHeaders(headers.get(@({:}))) + +proc createHeaders(headers: RawHeaders): string = + result = "" + if headers != nil: + for header in headers: + let (key, value) = header + result.add(key & ": " & value & "\c\L") + + result = result[0 .. ^3] # Strip trailing \c\L + +proc createResponse(status: HttpCode, headers: RawHeaders): string = + return "HTTP/1.1 " & $status & "\c\L" & createHeaders(headers) & "\c\L\c\L" + +proc unsafeSend(request: Request, content: string) = + when useHttpBeast: + request.getNativeReq.unsafeSend(content) + else: + # TODO: This may cause issues if we send too fast. + asyncCheck request.getNativeReq.client.send(content) + +proc send( + request: Request, code: HttpCode, headers: Option[RawHeaders], body: string +) = + when useHttpBeast: + let h = + if headers.isNone: "" + else: headers.get().createHeaders + request.getNativeReq.send(code, body, h) + else: + # TODO: This may cause issues if we send too fast. + asyncCheck request.getNativeReq.respond( + code, body, newHttpHeaders(headers.get(@({:}))) + ) + +proc statusContent(request: Request, status: HttpCode, content: string, + headers: Option[RawHeaders]) = + try: + send(request, status, headers, content) + when not defined(release): + logging.debug(" $1 $2" % [$status, toStr(headers)]) + except: + logging.error("Could not send response: $1" % osErrorMsg(osLastError())) + +# TODO: Add support for proper Future Streams instead of this weird raw mode. +template enableRawMode* = + # TODO: Use the effect system to make this implicit? + result.action = TCActionRaw + +proc send*(request: Request, content: string) = + ## Sends ``content`` immediately to the client socket. + ## + ## Routes using this procedure must enable raw mode. + unsafeSend(request, content) + +proc sendHeaders*(request: Request, status: HttpCode, + headers: RawHeaders) = + ## Sends ``status`` and ``headers`` to the client socket immediately. + ## The user is then able to send the content immediately to the client on + ## the fly through the use of ``response.client``. + let headerData = createResponse(status, headers) + try: + request.send(headerData) + logging.debug(" $1 $2" % [$status, $headers]) + except: + logging.error("Could not send response: $1" % [osErrorMsg(osLastError())]) + +proc sendHeaders*(request: Request, status: HttpCode) = + ## Sends ``status`` and ``Content-Type: text/html`` as the headers to the + ## client socket immediately. + let headers = @({"Content-Type": "text/html;charset=utf-8"}) + request.sendHeaders(status, headers) + +proc sendHeaders*(request: Request) = + ## Sends ``Http200`` and ``Content-Type: text/html`` as the headers to the + ## client socket immediately. + request.sendHeaders(Http200) + +proc send*(request: Request, status: HttpCode, headers: RawHeaders, + content: string) = + ## Sends out a HTTP response comprising of the ``status``, ``headers`` and + ## ``content`` specified. + var headers = headers & @({"Content-Length": $content.len}) + request.sendHeaders(status, headers) + request.send(content) + +# TODO: Cannot capture 'paths: varargs[string]' here. +proc sendStaticIfExists( + req: Request, paths: seq[string] +): Future[HttpCode] {.async.} = + result = Http200 + for p in paths: + if existsFile(p): + + var fp = getFilePermissions(p) + if not fp.contains(fpOthersRead): + return Http403 + + let fileSize = getFileSize(p) + let mimetype = req.settings.mimes.getMimetype(p.splitFile.ext[1 .. ^1]) + if fileSize < 10_000_000: # 10 mb + var file = readFile(p) + + var hashed = getMD5(file) + + # If the user has a cached version of this file and it matches our + # version, let them use it + if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed: + req.statusContent(Http304, "", none[RawHeaders]()) + else: + req.statusContent(Http200, file, some(@({ + "Content-Type": mimetype, + "ETag": hashed + }))) + else: + let headers = @({ + "Content-Type": mimetype, + "Content-Length": $fileSize + }) + req.statusContent(Http200, "", some(headers)) + + var fileStream = newFutureStream[string]("sendStaticIfExists") + var file = openAsync(p, fmRead) + # Let `readToStream` write file data into fileStream in the + # background. + asyncCheck file.readToStream(fileStream) + # The `writeFromStream` proc will complete once all the data in the + # `bodyStream` has been written to the file. + while true: + let (hasValue, value) = await fileStream.read() + if hasValue: + req.unsafeSend(value) + else: + break + file.close() + + return + + # If we get to here then no match could be found. + return Http404 + +proc defaultErrorFilter(error: RouteError): ResponseData = + case error.kind + of RouteException: + let e = error.exc + let traceback = getStackTrace(e) + var errorMsg = e.msg + if errorMsg.isNil: errorMsg = "(nil)" + + let error = traceback & errorMsg + logging.error(error) + result.headers = some(@({ + "Content-Type": "text/html;charset=utf-8" + })) + result.content = routeException( + error.replace("\n", "<br/>\n"), + jesterVer + ) + result.code = Http502 + result.matched = true + result.action = TCActionSend + of RouteCode: + result.headers = some(@({ + "Content-Type": "text/html;charset=utf-8" + })) + result.content = error( + $error.data.code, + jesterVer + ) + result.code = error.data.code + result.matched = true + result.action = TCActionSend + +proc initRouteError(exc: ref Exception): RouteError = + RouteError( + kind: RouteException, + exc: exc + ) + +proc initRouteError(data: ResponseData): RouteError = + RouteError( + kind: RouteCode, + data: data + ) + +proc dispatchError( + jes: Jester, + request: Request, + error: RouteError +): Future[ResponseData] {.async.} = + for errorProc in jes.errorHandlers: + let data = await errorProc(request, error) + if data.matched: + return data + + return defaultErrorFilter(error) + +proc dispatch( + self: Jester, + req: Request +): Future[ResponseData] {.async.} = + for matcher in self.matchers: + if matcher.async: + let data = await matcher.asyncProc(req) + if data.matched: + return data + else: + let data = matcher.syncProc(req) + if data.matched: + return data + +proc handleFileRequest( + jes: Jester, req: Request +): Future[ResponseData] {.async.} = + # Find static file. + # TODO: Caching. + let path = normalizedPath( + jes.settings.staticDir / cgi.decodeUrl(req.pathInfo) + ) + + # Verify that this isn't outside our static` dir. + var status = Http400 + if path.splitFile.dir.startsWith(jes.settings.staticDir): + if existsDir(path): + status = await sendStaticIfExists( + req, + @[path / "index.html", path / "index.htm"] + ) + else: + status = await sendStaticIfExists(req, @[path]) + + # Http200 means that the data was sent so there is nothing else to do. + if status == Http200: + result[0] = TCActionRaw + when not defined(release): + logging.debug(" -> $1" % path) + return + + return (TCActionSend, status, none[seq[(string, string)]](), "", true) + +proc handleRequestSlow( + jes: Jester, + req: Request, + respDataFut: Future[ResponseData] | ResponseData, + dispatchedError: bool +): Future[void] {.async.} = + var dispatchedError = dispatchedError + var respData: ResponseData + + # httpReq.send(Http200, "Hello, World!", "") + try: + when respDataFut is Future[ResponseData]: + respData = await respDataFut + else: + respData = respDataFut + except: + # Handle any errors by showing them in the browser. + # TODO: Improve the look of this. + let exc = getCurrentException() + respData = await dispatchError(jes, req, initRouteError(exc)) + dispatchedError = true + + # TODO: Put this in a custom matcher? + if not respData.matched: + respData = await handleFileRequest(jes, req) + + case respData.action + of TCActionSend: + if (respData.code.is4xx or respData.code.is5xx) and + not dispatchedError and respData.content.len == 0: + respData = await dispatchError(jes, req, initRouteError(respData)) + + statusContent( + req, + respData.code, + respData.content, + respData.headers + ) + else: + when not defined(release): + logging.debug(" $1" % [$respData.action]) + + # Cannot close the client socket. AsyncHttpServer may be keeping it alive. + +proc handleRequest(jes: Jester, httpReq: NativeRequest): Future[void] = + var req = initRequest(httpReq, jes.settings) + try: + when not defined(release): + logging.debug("$1 $2" % [$req.reqMethod, req.pathInfo]) + + if likely(jes.matchers.len == 1 and not jes.matchers[0].async): + let respData = jes.matchers[0].syncProc(req) + if likely(respData.matched): + statusContent( + req, + respData.code, + respData.content, + respData.headers + ) + else: + return handleRequestSlow(jes, req, respData, false) + else: + return handleRequestSlow(jes, req, dispatch(jes, req), false) + except: + let exc = getCurrentException() + let respDataFut = dispatchError(jes, req, initRouteError(exc)) + return handleRequestSlow(jes, req, respDataFut, true) + +proc newSettings*( + port = Port(5000), staticDir = getCurrentDir() / "public", + appName = "", bindAddr = "", reusePort = false, + futureErrorHandler: proc (fut: Future[void]) {.closure, gcsafe.} = nil +): Settings = + result = Settings( + staticDir: staticDir, + appName: appName, + port: port, + bindAddr: bindAddr, + reusePort: reusePort, + futureErrorHandler: futureErrorHandler + ) + +proc register*(self: var Jester, matcher: MatchProc) = + ## Adds the specified matcher procedure to the specified Jester instance. + self.matchers.add( + Matcher( + async: true, + asyncProc: matcher + ) + ) + +proc register*(self: var Jester, matcher: MatchProcSync) = + ## Adds the specified matcher procedure to the specified Jester instance. + self.matchers.add( + Matcher( + async: false, + syncProc: matcher + ) + ) + +proc register*(self: var Jester, errorHandler: ErrorProc) = + ## Adds the specified error handler procedure to the specified Jester instance. + self.errorHandlers.add(errorHandler) + +proc initJester*( + settings: Settings = newSettings() +): Jester = + result.settings = settings + result.settings.mimes = newMimetypes() + result.matchers = @[] + result.errorHandlers = @[] + +proc initJester*( + matcher: MatchProc, + settings: Settings = newSettings() +): Jester = + result = initJester(settings) + result.register(matcher) + +proc initJester*( + matcher: MatchProcSync, # TODO: Annoying nim bug: `MatchProc | MatchProcSync` doesn't work. + settings: Settings = newSettings() +): Jester = + result = initJester(settings) + result.register(matcher) + +proc serve*( + self: var Jester +) = + ## Creates a new async http server instance and registers + ## it with the dispatcher. + ## + ## The event loop is executed by this function, so it will block forever. + + # Ensure we have at least one logger enabled, defaulting to console. + if logging.getHandlers().len == 0: + addHandler(logging.newConsoleLogger()) + setLogFilter(when defined(release): lvlInfo else: lvlDebug) + + if self.settings.bindAddr.len > 0: + logging.info("Jester is making jokes at http://$1:$2$3" % + [ + self.settings.bindAddr, $self.settings.port, self.settings.appName + ] + ) + else: + logging.info("Jester is making jokes at http://0.0.0.0:$1$2" % + [$self.settings.port, self.settings.appName]) + + var jes = self + when useHttpBeast: + run( + proc (req: httpbeast.Request): Future[void] = + result = handleRequest(jes, req), + httpbeast.initSettings(self.settings.port, self.settings.bindAddr) + ) + else: + self.httpServer = newAsyncHttpServer(reusePort=self.settings.reusePort) + let serveFut = self.httpServer.serve( + self.settings.port, + proc (req: asynchttpserver.Request): Future[void] {.gcsafe, closure.} = + result = handleRequest(jes, req), + self.settings.bindAddr) + if not self.settings.futureErrorHandler.isNil: + serveFut.callback = self.settings.futureErrorHandler + else: + asyncCheck serveFut + runForever() + +template resp*(code: HttpCode, + headers: openarray[tuple[key, value: string]], + content: string): typed = + ## Sets ``(code, headers, content)`` as the response. + bind TCActionSend, newHttpHeaders + result = (TCActionSend, code, headers.newHttpHeaders.some(), content, true) + break route + +template setHeader(headers: var Option[RawHeaders], key, value: string): typed = + bind isNone + if isNone(headers): + headers = some(@({key: value})) + else: + block outer: + # Overwrite key if it exists. + var h = headers.get() + for i in 0 ..< h.len: + if h[i][0] == key: + h[i][1] = value + headers = some(h) + break outer + + # Add key if it doesn't exist. + headers = some(h & @({key: value})) + +template resp*(content: string, contentType = "text/html;charset=utf-8"): typed = + ## Sets ``content`` as the response; ``Http200`` as the status code + ## and ``contentType`` as the Content-Type. + bind TCActionSend, newHttpHeaders, strtabs.`[]=` + result[0] = TCActionSend + result[1] = Http200 + setHeader(result[2], "Content-Type", contentType) + result[3] = content + # This will be set by our macro, so this is here for those not using it. + result.matched = true + break route + +template resp*(content: JsonNode): typed = + ## Serializes ``content`` as the response, sets ``Http200`` as status code + ## and "application/json" Content-Type. + resp($content, contentType="application/json") + +template resp*(code: HttpCode, content: string, + contentType = "text/html;charset=utf-8"): typed = + ## Sets ``content`` as the response; ``code`` as the status code + ## and ``contentType`` as the Content-Type. + bind TCActionSend, newHttpHeaders + result[0] = TCActionSend + result[1] = code + setHeader(result[2], "Content-Type", contentType) + result[3] = content + result.matched = true + break route + +template resp*(code: HttpCode): typed = + ## Responds with the specified ``HttpCode``. This ensures that error handlers + ## are called. + bind TCActionSend, newHttpHeaders + result[0] = TCActionSend + result[1] = code + result.matched = true + break route + +template redirect*(url: string): typed = + ## Redirects to ``url``. Returns from this request handler immediately. + ## Any set response headers are preserved for this request. + bind TCActionSend, newHttpHeaders + result[0] = TCActionSend + result[1] = Http303 + setHeader(result[2], "Location", url) + result[3] = "" + result.matched = true + break route + +template pass*(): typed = + ## Skips this request handler. + ## + ## If you want to stop this request from going further use ``halt``. + result.action = TCActionPass + break outerRoute + +template cond*(condition: bool): typed = + ## If ``condition`` is ``False`` then ``pass`` will be called, + ## i.e. this request handler will be skipped. + if not condition: break outerRoute + +template halt*(code: HttpCode, + headers: openarray[tuple[key, val: string]], + content: string): typed = + ## Immediately replies with the specified request. This means any further + ## code will not be executed after calling this template in the current + ## route. + bind TCActionSend, newHttpHeaders + result[0] = TCActionSend + result[1] = code + result[2] = some(@headers) + result[3] = content + result.matched = true + break allRoutes + +template halt*(): typed = + ## Halts the execution of this request immediately. Returns a 404. + ## All previously set values are **discarded**. + halt(Http404, {"Content-Type": "text/html;charset=utf-8"}, error($Http404, jesterVer)) + +template halt*(code: HttpCode): typed = + halt(code, {"Content-Type": "text/html;charset=utf-8"}, error($code, jesterVer)) + +template halt*(content: string): typed = + halt(Http404, {"Content-Type": "text/html;charset=utf-8"}, content) + +template halt*(code: HttpCode, content: string): typed = + halt(code, {"Content-Type": "text/html;charset=utf-8"}, content) + +template attachment*(filename = ""): typed = + ## Instructs the browser that the response should be stored on disk + ## rather than displayed in the browser. + var disposition = "attachment" + if filename != "": + disposition.add("; filename=\"" & extractFilename(filename) & "\"") + let ext = splitFile(filename).ext + let contentTypeSet = + isSome(result[2]) and result[2].get().toTable.hasKey("Content-Type") + if not contentTypeSet and ext != "": + setHeader(result[2], "Content-Type", getMimetype(request.settings.mimes, ext)) + setHeader(result[2], "Content-Disposition", disposition) + +template sendFile*(filename: string): typed = + ## Sends the file at the specified filename as the response. + result[0] = TCActionRaw + let sendFut = sendStaticIfExists(request, @[filename]) + yield sendFut + let status = sendFut.read() + if status != Http200: + raise newException(JesterError, "Couldn't send requested file: " & filename) + # This will be set by our macro, so this is here for those not using it. + result.matched = true + break route + +template `@`*(s: string): untyped = + ## Retrieves the parameter ``s`` from ``request.params``. ``""`` will be + ## returned if parameter doesn't exist. + if s in params(request): + # TODO: Why does request.params not work? :( + # TODO: This is some weird bug with macros/templates, I couldn't + # TODO: reproduce it easily. + params(request)[s] + else: + "" + +proc setStaticDir*(request: Request, dir: string) = + ## Sets the directory in which Jester will look for static files. It is + ## ``./public`` by default. + ## + ## The files will be served like so: + ## + ## ./public/css/style.css ``->`` http://example.com/css/style.css + ## + ## (``./public`` is not included in the final URL) + request.settings.staticDir = dir + +proc getStaticDir*(request: Request): string = + ## Gets the directory in which Jester will look for static files. + ## + ## ``./public`` by default. + return request.settings.staticDir + +proc makeUri*(request: Request, address = "", absolute = true, + addScriptName = true): string = + ## Creates a URI based on the current request. If ``absolute`` is true it will + ## add the scheme (Usually 'http://'), `request.host` and `request.port`. + ## If ``addScriptName`` is true `request.appName` will be prepended before + ## ``address``. + + # Check if address already starts with scheme:// + var uri = parseUri(address) + + if uri.scheme != "": return address + uri.path = "/" + uri.query = "" + uri.anchor = "" + if absolute: + uri.hostname = request.host + uri.scheme = (if request.secure: "https" else: "http") + if request.port != (if request.secure: 443 else: 80): + uri.port = $request.port + + if addScriptName: uri = uri / request.appName + if address != "": + uri = uri / address + else: + uri = uri / request.pathInfo + return $uri + +template uri*(address = "", absolute = true, addScriptName = true): untyped = + ## Convenience template which can be used in a route. + request.makeUri(address, absolute, addScriptName) + +proc daysForward*(days: int): DateTime = + ## Returns a DateTime object referring to the current time plus ``days``. + return getTime().utc + initInterval(days = days) + +template setCookie*(name, value: string, expires="", + sameSite: SameSite=Lax): typed = + ## Creates a cookie which stores ``value`` under ``name``. + ## + ## The SameSite argument determines the level of CSRF protection that + ## you wish to adopt for this cookie. It's set to Lax by default which + ## should protect you from most vulnerabilities. Note that this is only + ## supported by some browsers: + ## https://caniuse.com/#feat=same-site-cookie-attribute + let newCookie = makeCookie(name, value, expires) + if isSome(result[2]) and + (let headers = result[2].get(); headers.toTable.hasKey("Set-Cookie")): + result[2] = some(headers & @({"Set-Cookie": newCookie})) + else: + setHeader(result[2], "Set-Cookie", newCookie) + +template setCookie*(name, value: string, expires: DateTime, + sameSite: SameSite=Lax): typed = + ## Creates a cookie which stores ``value`` under ``name``. + setCookie(name, value, format(expires, "ddd',' dd MMM yyyy HH:mm:ss 'GMT'")) + +proc normalizeUri*(uri: string): string = + ## Remove any trailing ``/``. + if uri[uri.len-1] == '/': result = uri[0 .. uri.len-2] + else: result = uri + +# -- Macro + +proc checkAction*(respData: var ResponseData): bool = + case respData.action + of TCActionSend, TCActionRaw: + result = true + of TCActionPass: + result = false + of TCActionNothing: + assert(false) + +proc skipDo(node: NimNode): NimNode {.compiletime.} = + if node.kind == nnkDo: + result = node[6] + else: + result = node + +proc ctParsePattern(pattern, pathPrefix: string): NimNode {.compiletime.} = + result = newNimNode(nnkPrefix) + result.add newIdentNode("@") + result.add newNimNode(nnkBracket) + + proc addPattNode(res: var NimNode, typ, text, + optional: NimNode) {.compiletime.} = + var objConstr = newNimNode(nnkObjConstr) + + objConstr.add bindSym("Node") + objConstr.add newNimNode(nnkExprColonExpr).add( + newIdentNode("typ"), typ) + objConstr.add newNimNode(nnkExprColonExpr).add( + newIdentNode("text"), text) + objConstr.add newNimNode(nnkExprColonExpr).add( + newIdentNode("optional"), optional) + + res[1].add objConstr + + var patt = parsePattern(pattern) + if pathPrefix.len > 0: + result.addPattNode( + bindSym("NodeText"), # Node kind + newStrLitNode(pathPrefix), # Text + newIdentNode("false") # Optional? + ) + + for node in patt: + result.addPattNode( + case node.typ + of NodeText: bindSym("NodeText") + of NodeField: bindSym("NodeField"), + newStrLitNode(node.text), + newIdentNode(if node.optional: "true" else: "false")) + +template setDefaultResp*(): typed = + # TODO: bindSym this in the 'routes' macro and put it in each route + bind TCActionNothing, newHttpHeaders + result.action = TCActionNothing + result.code = Http200 + result.content = "" + +template declareSettings(): typed {.dirty.} = + when not declaredInScope(settings): + var settings = newSettings() + +proc createJesterPattern( + routeNode, patternMatchSym: NimNode, + pathPrefix: string +): NimNode {.compileTime.} = + var ctPattern = ctParsePattern(routeNode[1].strVal, pathPrefix) + # -> let <patternMatchSym> = <ctPattern>.match(request.path) + return newLetStmt(patternMatchSym, + newCall(bindSym"match", ctPattern, parseExpr("request.pathInfo"))) + +proc escapeRegex(s: string): string = + result = "" + for i in s: + case i + # https://stackoverflow.com/a/400316/492186 + of '.', '^', '$', '*', '+', '?', '(', ')', '[', '{', '\\', '|': + result.add('\\') + result.add(i) + else: + result.add(i) + +proc createRegexPattern( + routeNode, reMatchesSym, patternMatchSym: NimNode, + pathPrefix: string +): NimNode {.compileTime.} = + # -> let <patternMatchSym> = find(request.pathInfo, <pattern>, <reMatches>) + var strNode = routeNode[1].copyNimTree() + strNode[1].strVal = escapeRegex(pathPrefix) & strNode[1].strVal + return newLetStmt( + patternMatchSym, + newCall( + bindSym"find", + parseExpr("request.pathInfo"), + strNode, + reMatchesSym + ) + ) + +proc determinePatternType(pattern: NimNode): MatchType {.compileTime.} = + case pattern.kind + of nnkStrLit: + var patt = parsePattern(pattern.strVal) + if patt.len == 1 and patt[0].typ == NodeText: + return MStatic + else: + return MSpecial + of nnkCallStrLit: + expectKind(pattern[0], nnkIdent) + case ($pattern[0]).normalize + of "re": return MRegex + else: + macros.error("Invalid pattern type: " & $pattern[0]) + else: + macros.error("Unexpected node kind: " & $pattern.kind) + +proc createCheckActionIf(): NimNode = + var checkActionIf = parseExpr( + "if checkAction(result): result.matched = true; break routesList" + ) + checkActionIf[0][0][0] = bindSym"checkAction" + return checkActionIf + +proc createGlobalMetaRoute(routeNode, dest: NimNode) {.compileTime.} = + ## Creates a ``before`` or ``after`` route with no pattern, i.e. one which + ## will be always executed. + + # -> block route: <ifStmtBody> + var innerBlockStmt = newStmtList( + newNimNode(nnkBlockStmt).add(newIdentNode("route"), routeNode[1].skipDo()) + ) + + # -> block outerRoute: <innerBlockStmt> + var blockStmt = newNimNode(nnkBlockStmt).add( + newIdentNode("outerRoute"), innerBlockStmt) + dest.add blockStmt + +proc createRoute( + routeNode, dest: NimNode, pathPrefix: string, isMetaRoute: bool = false +) {.compileTime.} = + ## Creates code which checks whether the current request path + ## matches a route. + ## + ## The `isMetaRoute` parameter determines whether the route to be created is + ## one of either a ``before`` or an ``after`` route. + + var patternMatchSym = genSym(nskLet, "patternMatchRet") + + # Only used for Regex patterns. + var reMatchesSym = genSym(nskVar, "reMatches") + var reMatches = parseExpr("var reMatches: array[20, string]") + reMatches[0][0] = reMatchesSym + reMatches[0][1][1] = bindSym("MaxSubpatterns") + + let patternType = determinePatternType(routeNode[1]) + case patternType + of MStatic: + discard + of MSpecial: + dest.add createJesterPattern(routeNode, patternMatchSym, pathPrefix) + of MRegex: + dest.add reMatches + dest.add createRegexPattern( + routeNode, reMatchesSym, patternMatchSym, pathPrefix + ) + + var ifStmtBody = newStmtList() + case patternType + of MStatic: discard + of MSpecial: + # -> setPatternParams(request, ret.params) + ifStmtBody.add newCall(bindSym"setPatternParams", newIdentNode"request", + newDotExpr(patternMatchSym, newIdentNode"params")) + of MRegex: + # -> setReMatches(request, <reMatchesSym>) + ifStmtBody.add newCall(bindSym"setReMatches", newIdentNode"request", + reMatchesSym) + + ifStmtBody.add routeNode[2].skipDo() + + let checkActionIf = + if isMetaRoute: + parseExpr("break routesList") + else: + createCheckActionIf() + # -> block route: <ifStmtBody>; <checkActionIf> + var innerBlockStmt = newStmtList( + newNimNode(nnkBlockStmt).add(newIdentNode("route"), ifStmtBody), + checkActionIf + ) + + let ifCond = + case patternType + of MStatic: + infix( + parseExpr("request.pathInfo"), + "==", + newStrLitNode(pathPrefix & routeNode[1].strVal) + ) + of MSpecial: + newDotExpr(patternMatchSym, newIdentNode("matched")) + of MRegex: + infix(patternMatchSym, "!=", newIntLitNode(-1)) + + # -> if <patternMatchSym>.matched: <innerBlockStmt> + var ifStmt = newIfStmt((ifCond, innerBlockStmt)) + + # -> block outerRoute: <ifStmt> + var blockStmt = newNimNode(nnkBlockStmt).add( + newIdentNode("outerRoute"), ifStmt) + dest.add blockStmt + +proc createError( + errorNode: NimNode, + httpCodeBranches, + exceptionBranches: var seq[tuple[cond, body: NimNode]] +) = + if errorNode.len != 3: + error("Missing error condition or body.", errorNode) + + let routeIdent = newIdentNode("route") + let outerRouteIdent = newIdentNode("outerRoute") + let checkActionIf = createCheckActionIf() + let exceptionIdent = newIdentNode("exception") + let errorIdent = newIdentNode("error") # TODO: Ugh. I shouldn't need these... + let errorCond = errorNode[1] + let errorBody = errorNode[2] + let body = quote do: + block `outerRouteIdent`: + block `routeIdent`: + `errorBody` + `checkActionIf` + + case errorCond.kind + of nnkIdent: + let name = errorCond.strVal + if name.len == 7 and name.startsWith("Http"): + # HttpCode. + httpCodeBranches.add( + ( + infix(parseExpr("error.data.code"), "==", errorCond), + body + ) + ) + else: + # Exception + exceptionBranches.add( + ( + infix(parseExpr("error.exc"), "of", errorCond), + quote do: + let `exceptionIdent` = (ref `errorCond`)(`errorIdent`.exc) + `body` + ) + ) + of nnkCurly: + expectKind(errorCond[0], nnkInfix) + httpCodeBranches.add( + ( + infix(parseExpr("error.data.code"), "in", errorCond), + body + ) + ) + else: + error("Expected exception type or set[HttpCode].", errorCond) + +const definedRoutes = CacheTable"jester.routes" + +proc processRoutesBody( + body: NimNode, + # For HTTP methods. + caseStmtGetBody, + caseStmtPostBody, + caseStmtPutBody, + caseStmtDeleteBody, + caseStmtHeadBody, + caseStmtOptionsBody, + caseStmtTraceBody, + caseStmtConnectBody, + caseStmtPatchBody: var NimNode, + # For `error`. + httpCodeBranches, + exceptionBranches: var seq[tuple[cond, body: NimNode]], + # For before/after stmts. + beforeStmts, + afterStmts: var NimNode, + # For other statements. + outsideStmts: var NimNode, + pathPrefix: string +) = + for i in 0..<body.len: + case body[i].kind + of nnkCall: + let cmdName = body[i][0].`$`.normalize + case cmdName + of "before": + createGlobalMetaRoute(body[i], beforeStmts) + of "after": + createGlobalMetaRoute(body[i], afterStmts) + else: + outsideStmts.add(body[i]) + of nnkCommand: + let cmdName = body[i][0].`$`.normalize + case cmdName + # HTTP Methods + of "get": + createRoute(body[i], caseStmtGetBody, pathPrefix) + of "post": + createRoute(body[i], caseStmtPostBody, pathPrefix) + of "put": + createRoute(body[i], caseStmtPutBody, pathPrefix) + of "delete": + createRoute(body[i], caseStmtDeleteBody, pathPrefix) + of "head": + createRoute(body[i], caseStmtHeadBody, pathPrefix) + of "options": + createRoute(body[i], caseStmtOptionsBody, pathPrefix) + of "trace": + createRoute(body[i], caseStmtTraceBody, pathPrefix) + of "connect": + createRoute(body[i], caseStmtConnectBody, pathPrefix) + of "patch": + createRoute(body[i], caseStmtPatchBody, pathPrefix) + # Other + of "error": + createError(body[i], httpCodeBranches, exceptionBranches) + of "before": + createRoute(body[i], beforeStmts, pathPrefix, isMetaRoute=true) + of "after": + createRoute(body[i], afterStmts, pathPrefix, isMetaRoute=true) + of "extend": + # Extend another router. + let extend = body[i] + if extend[1].kind != nnkIdent: + error("Expected identifier.", extend[1]) + + let prefix = + if extend.len > 1: + extend[2].strVal + else: + "" + if prefix.len != 0 and prefix[0] != '/': + error("Path prefix for extended route must start with '/'", extend[2]) + + processRoutesBody( + definedRoutes[extend[1].strVal], + caseStmtGetBody, + caseStmtPostBody, + caseStmtPutBody, + caseStmtDeleteBody, + caseStmtHeadBody, + caseStmtOptionsBody, + caseStmtTraceBody, + caseStmtConnectBody, + caseStmtPatchBody, + httpCodeBranches, + exceptionBranches, + beforeStmts, + afterStmts, + outsideStmts, + pathPrefix & prefix + ) + else: + outsideStmts.add(body[i]) + of nnkCommentStmt: + discard + of nnkPragma: + if body[i][0].strVal.normalize notin ["async", "sync"]: + outsideStmts.add(body[i]) + else: + outsideStmts.add(body[i]) + +type + NeedsAsync = enum + ImplicitTrue, ImplicitFalse, ExplicitTrue, ExplicitFalse +proc needsAsync(node: NimNode): NeedsAsync = + result = ImplicitFalse + case node.kind + of nnkCommand, nnkCall: + if node[0].kind == nnkIdent: + case node[0].strVal.normalize + of "await", "sendfile": + return ImplicitTrue + of "resp", "halt", "attachment", "pass", "redirect", "cond", "get", + "post", "patch", "delete": + # This is just a simple heuristic. It's by no means meant to be + # exhaustive. + discard + else: + return ImplicitTrue + of nnkYieldStmt: + return ImplicitTrue + of nnkPragma: + if node[0].kind == nnkIdent: + case node[0].strVal.normalize + of "sync": + return ExplicitFalse + of "async": + return ExplicitTrue + else: discard + else: discard + + for c in node: + let r = needsAsync(c) + if r in {ImplicitTrue, ExplicitTrue, ExplicitFalse}: return r + +proc routesEx(name: string, body: NimNode): NimNode = + # echo(treeRepr(body)) + # echo(treeRepr(name)) + + # Save this route's body so that it can be incorporated into another route. + definedRoutes[name] = body.copyNimTree + + result = newStmtList() + + # -> declareSettings() + result.add newCall(bindSym"declareSettings") + + var outsideStmts = newStmtList() + + var matchBody = newNimNode(nnkStmtList) + let setDefaultRespIdent = bindSym"setDefaultResp" + matchBody.add newCall(setDefaultRespIdent) + # TODO: This diminishes the performance. Would be nice to only include it + # TODO: when setPatternParams or setReMatches is used. + matchBody.add parseExpr("var request = request") + + # HTTP router case statement nodes: + var caseStmt = newNimNode(nnkCaseStmt) + caseStmt.add parseExpr("request.reqMethod") + + var caseStmtGetBody = newNimNode(nnkStmtList) + var caseStmtPostBody = newNimNode(nnkStmtList) + var caseStmtPutBody = newNimNode(nnkStmtList) + var caseStmtDeleteBody = newNimNode(nnkStmtList) + var caseStmtHeadBody = newNimNode(nnkStmtList) + var caseStmtOptionsBody = newNimNode(nnkStmtList) + var caseStmtTraceBody = newNimNode(nnkStmtList) + var caseStmtConnectBody = newNimNode(nnkStmtList) + var caseStmtPatchBody = newNimNode(nnkStmtList) + + # Error handler nodes: + var httpCodeBranches: seq[tuple[cond, body: NimNode]] = @[] + var exceptionBranches: seq[tuple[cond, body: NimNode]] = @[] + + # Before/After nodes: + var beforeRoutes = newStmtList() + var afterRoutes = newStmtList() + + processRoutesBody( + body, + caseStmtGetBody, + caseStmtPostBody, + caseStmtPutBody, + caseStmtDeleteBody, + caseStmtHeadBody, + caseStmtOptionsBody, + caseStmtTraceBody, + caseStmtConnectBody, + caseStmtPatchBody, + httpCodeBranches, + exceptionBranches, + beforeRoutes, + afterRoutes, + outsideStmts, + "" + ) + + var ofBranchGet = newNimNode(nnkOfBranch) + ofBranchGet.add newIdentNode("HttpGet") + ofBranchGet.add caseStmtGetBody + caseStmt.add ofBranchGet + + var ofBranchPost = newNimNode(nnkOfBranch) + ofBranchPost.add newIdentNode("HttpPost") + ofBranchPost.add caseStmtPostBody + caseStmt.add ofBranchPost + + var ofBranchPut = newNimNode(nnkOfBranch) + ofBranchPut.add newIdentNode("HttpPut") + ofBranchPut.add caseStmtPutBody + caseStmt.add ofBranchPut + + var ofBranchDelete = newNimNode(nnkOfBranch) + ofBranchDelete.add newIdentNode("HttpDelete") + ofBranchDelete.add caseStmtDeleteBody + caseStmt.add ofBranchDelete + + var ofBranchHead = newNimNode(nnkOfBranch) + ofBranchHead.add newIdentNode("HttpHead") + ofBranchHead.add caseStmtHeadBody + caseStmt.add ofBranchHead + + var ofBranchOptions = newNimNode(nnkOfBranch) + ofBranchOptions.add newIdentNode("HttpOptions") + ofBranchOptions.add caseStmtOptionsBody + caseStmt.add ofBranchOptions + + var ofBranchTrace = newNimNode(nnkOfBranch) + ofBranchTrace.add newIdentNode("HttpTrace") + ofBranchTrace.add caseStmtTraceBody + caseStmt.add ofBranchTrace + + var ofBranchConnect = newNimNode(nnkOfBranch) + ofBranchConnect.add newIdentNode("HttpConnect") + ofBranchConnect.add caseStmtConnectBody + caseStmt.add ofBranchConnect + + var ofBranchPatch = newNimNode(nnkOfBranch) + ofBranchPatch.add newIdentNode("HttpPatch") + ofBranchPatch.add caseStmtPatchBody + caseStmt.add ofBranchPatch + + # Wrap the routes inside ``routesList`` blocks accordingly, and add them to + # the `match` procedure body. + let routesListIdent = newIdentNode("routesList") + matchBody.add( + quote do: + block `routesListIdent`: + `beforeRoutes` + ) + + matchBody.add( + quote do: + block `routesListIdent`: + `caseStmt` + ) + + matchBody.add( + quote do: + block `routesListIdent`: + `afterRoutes` + ) + + let matchIdent = newIdentNode(name) + let reqIdent = newIdentNode("request") + let needsAsync = needsAsync(body) + case needsAsync + of ImplicitFalse, ExplicitFalse: + hint(fmt"Synchronous route `{name}` has been optimised. Use `{{.async.}}` to change.") + of ImplicitTrue, ExplicitTrue: + hint(fmt"Asynchronous route: {name}.") + var matchProc = + if needsAsync in {ImplicitTrue, ExplicitTrue}: + quote do: + proc `matchIdent`( + `reqIdent`: Request + ): Future[ResponseData] {.async, gcsafe.} = + discard + else: + quote do: + proc `matchIdent`( + `reqIdent`: Request + ): ResponseData {.gcsafe.} = + discard + + # The following `block` is for `halt`. (`return` didn't work :/) + let allRoutesBlock = newTree( + nnkBlockStmt, + newIdentNode("allRoutes"), + matchBody + ) + matchProc[6] = newTree(nnkStmtList, allRoutesBlock) + result.add(outsideStmts) + result.add(matchProc) + + # Error handler proc + let errorHandlerIdent = newIdentNode(name & "ErrorHandler") + let errorIdent = newIdentNode("error") + let exceptionIdent = newIdentNode("exception") + let resultIdent = newIdentNode("result") + var errorHandlerProc = quote do: + proc `errorHandlerIdent`( + `reqIdent`: Request, `errorIdent`: RouteError + ): Future[ResponseData] {.gcsafe, async.} = + block `routesListIdent`: + `setDefaultRespIdent`() + case `errorIdent`.kind + of RouteException: + discard + of RouteCode: + discard + if exceptionBranches.len != 0: + var stmts = newStmtList() + for branch in exceptionBranches: + stmts.add(newIfStmt(branch)) + errorHandlerProc[6][0][1][^1][1][1][0] = stmts + if httpCodeBranches.len > 1: + var stmts = newStmtList() + for branch in httpCodeBranches: + stmts.add(newIfStmt(branch)) + errorHandlerProc[6][0][1][^1][2][1][0] = stmts + result.add(errorHandlerProc) + + # TODO: Replace `body`, `headers`, `code` in routes with `result[i]` to + # get these shortcuts back without sacrificing usability. + # TODO2: Make sure you replace what `guessAction` used to do for this. + + # echo toStrLit(result) + # echo treeRepr(result) + +macro routes*(body: untyped): typed = + result = routesEx("match", body) + let jesIdent = genSym(nskVar, "jes") + let matchIdent = newIdentNode("match") + let errorHandlerIdent = newIdentNode("matchErrorHandler") + let settingsIdent = newIdentNode("settings") + result.add( + quote do: + var `jesIdent` = initJester(`matchIdent`, `settingsIdent`) + `jesIdent`.register(`errorHandlerIdent`) + ) + result.add( + quote do: + serve(`jesIdent`) + ) + +macro router*(name: untyped, body: untyped): typed = + if name.kind != nnkIdent: + error("Need an ident.", name) + + routesEx($name.ident, body) + +macro settings*(body: untyped): typed = + #echo(treeRepr(body)) + expectKind(body, nnkStmtList) + + result = newStmtList() + + # var settings = newSettings() + let settingsIdent = newIdentNode("settings") + result.add newVarStmt(settingsIdent, newCall("newSettings")) + + for asgn in body.children: + expectKind(asgn, nnkAsgn) + result.add newAssignment(newDotExpr(settingsIdent, asgn[0]), asgn[1]) diff --git a/tests/deps/jester-#head/jester.nimble b/tests/deps/jester-#head/jester.nimble new file mode 100644 index 000000000..08e990bd7 --- /dev/null +++ b/tests/deps/jester-#head/jester.nimble @@ -0,0 +1,22 @@ +# Package + +version = "0.4.0" # Be sure to update jester.jesterVer too! +author = "Dominik Picheta" +description = "A sinatra-like web framework for Nim." +license = "MIT" + +skipFiles = @["todo.markdown"] +skipDirs = @["tests"] + +# Deps + +requires "nim >= 0.18.1" + +when not defined(windows): + requires "httpbeast >= 0.2.0" + +# For tests +requires "asynctools" + +task test, "Runs the test suite.": + exec "nimble c -y -r tests/tester" \ No newline at end of file diff --git a/tests/deps/jester-#head/jester/patterns.nim b/tests/deps/jester-#head/jester/patterns.nim new file mode 100644 index 000000000..52b0d3a15 --- /dev/null +++ b/tests/deps/jester-#head/jester/patterns.nim @@ -0,0 +1,141 @@ +# Copyright (C) 2012-2018 Dominik Picheta +# MIT License - Look at license.txt for details. +import parseutils, tables +type + NodeType* = enum + NodeText, NodeField + Node* = object + typ*: NodeType + text*: string + optional*: bool + + Pattern* = seq[Node] + +#/show/@id/? +proc parsePattern*(pattern: string): Pattern = + result = @[] + template addNode(result: var Pattern, theT: NodeType, theText: string, + isOptional: bool): typed = + block: + var newNode: Node + newNode.typ = theT + newNode.text = theText + newNode.optional = isOptional + result.add(newNode) + + template `{}`(s: string, i: int): char = + if i >= len(s): + '\0' + else: + s[i] + + var i = 0 + var text = "" + while i < pattern.len(): + case pattern[i] + of '@': + # Add the stored text. + if text != "": + result.addNode(NodeText, text, false) + text = "" + # Parse named parameter. + inc(i) # Skip @ + var nparam = "" + i += pattern.parseUntil(nparam, {'/', '?'}, i) + var optional = pattern{i} == '?' + result.addNode(NodeField, nparam, optional) + if pattern{i} == '?': inc(i) # Only skip ?. / should not be skipped. + of '?': + var optionalChar = text[^1] + setLen(text, text.len-1) # Truncate ``text``. + # Add the stored text. + if text != "": + result.addNode(NodeText, text, false) + text = "" + # Add optional char. + inc(i) # Skip ? + result.addNode(NodeText, $optionalChar, true) + of '\\': + inc i # Skip \ + if pattern[i] notin {'?', '@', '\\'}: + raise newException(ValueError, + "This character does not require escaping: " & pattern[i]) + text.add(pattern{i}) + inc i # Skip ``pattern[i]`` + else: + text.add(pattern{i}) + inc(i) + + if text != "": + result.addNode(NodeText, text, false) + +proc findNextText(pattern: Pattern, i: int, toNode: var Node): bool = + ## Finds the next NodeText in the pattern, starts looking from ``i``. + result = false + for n in i..pattern.len()-1: + if pattern[n].typ == NodeText: + toNode = pattern[n] + return true + +proc check(n: Node, s: string, i: int): bool = + let cutTo = (n.text.len-1)+i + if cutTo > s.len-1: return false + return s.substr(i, cutTo) == n.text + +proc match*(pattern: Pattern, s: string): + tuple[matched: bool, params: Table[string, string]] = + var i = 0 # Location in ``s``. + + result.matched = true + result.params = initTable[string, string]() + + for ncount, node in pattern: + case node.typ + of NodeText: + if node.optional: + if check(node, s, i): + inc(i, node.text.len) # Skip over this optional character. + else: + # If it's not there, we have nothing to do. It's optional after all. + discard + else: + if check(node, s, i): + inc(i, node.text.len) # Skip over this + else: + # No match. + result.matched = false + return + of NodeField: + var nextTxtNode: Node + var stopChar = '/' + if findNextText(pattern, ncount, nextTxtNode): + stopChar = nextTxtNode.text[0] + var matchNamed = "" + i += s.parseUntil(matchNamed, stopChar, i) + result.params[node.text] = matchNamed + if matchNamed == "" and not node.optional: + result.matched = false + return + + if s.len != i: + result.matched = false + +when isMainModule: + let f = parsePattern("/show/@id/test/@show?/?") + doAssert match(f, "/show/12/test/hallo/").matched + doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched + doAssert(not match(f, "/").matched) + doAssert(not match(f, "/show//test//").matched) + doAssert(match(f, "/show/asd/test//").matched) + doAssert(not match(f, "/show/asd/asd/test/jjj/").matched) + doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←") + + let f2 = parsePattern("/test42/somefile.?@ext?/?") + doAssert(match(f2, "/test42/somefile/").params["ext"] == "") + doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt") + doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt") + + let f3 = parsePattern(r"/test32/\@\\\??") + doAssert(match(f3, r"/test32/@\").matched) + doAssert(not match(f3, r"/test32/@\\").matched) + doAssert(match(f3, r"/test32/@\?").matched) diff --git a/tests/deps/jester-#head/jester/private/errorpages.nim b/tests/deps/jester-#head/jester/private/errorpages.nim new file mode 100644 index 000000000..d1e695040 --- /dev/null +++ b/tests/deps/jester-#head/jester/private/errorpages.nim @@ -0,0 +1,19 @@ +# Copyright (C) 2012 Dominik Picheta +# MIT License - Look at license.txt for details. +import htmlgen +proc error*(err, jesterVer: string): string = + return html(head(title(err)), + body(h1(err), + "<hr/>", + p("Jester " & jesterVer), + style = "text-align: center;" + ), + xmlns="http://www.w3.org/1999/xhtml") + +proc routeException*(error: string, jesterVer: string): string = + return html(head(title("Jester route exception")), + body( + h1("An error has occured in one of your routes."), + p(b("Detail: "), error) + ), + xmlns="http://www.w3.org/1999/xhtml") diff --git a/tests/deps/jester-#head/jester/private/utils.nim b/tests/deps/jester-#head/jester/private/utils.nim new file mode 100644 index 000000000..66f0b37a6 --- /dev/null +++ b/tests/deps/jester-#head/jester/private/utils.nim @@ -0,0 +1,195 @@ +# 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 isMainModule: + var r = {:}.newStringTable + parseUrlQuery("FirstName=Mickey", r) + echo r + diff --git a/tests/deps/jester-#head/jester/request.nim b/tests/deps/jester-#head/jester/request.nim new file mode 100644 index 000000000..1b837d728 --- /dev/null +++ b/tests/deps/jester-#head/jester/request.nim @@ -0,0 +1,184 @@ +import uri, cgi, tables, logging, strutils, re, options + +import jester/private/utils + +when useHttpBeast: + import httpbeast except Settings + import options, httpcore + + type + NativeRequest* = httpbeast.Request +else: + import asynchttpserver + + type + NativeRequest* = asynchttpserver.Request + +type + Request* = object + req: NativeRequest + patternParams: Option[Table[string, string]] + reMatches: array[MaxSubpatterns, string] + settings*: Settings + +proc body*(req: Request): string = + ## Body of the request, only for POST. + ## + ## You're probably looking for ``formData`` + ## instead. + when useHttpBeast: + req.req.body.get("") + else: + req.req.body + +proc headers*(req: Request): HttpHeaders = + ## Headers received with the request. + ## Retrieving these is case insensitive. + when useHttpBeast: + if req.req.headers.isNone: + newHttpHeaders() + else: + req.req.headers.get() + else: + req.req.headers + +proc path*(req: Request): string = + ## Path of request without the query string. + when useHttpBeast: + let p = req.req.path.get("") + let queryStart = p.find('?') + if unlikely(queryStart != -1): + return p[0 .. queryStart-1] + else: + return p + else: + let u = req.req.url + return u.path + +proc reqMethod*(req: Request): HttpMethod = + ## Request method, eg. HttpGet, HttpPost + when useHttpBeast: + req.req.httpMethod.get() + else: + req.req.reqMethod +proc reqMeth*(req: Request): HttpMethod {.deprecated.} = + req.reqMethod + +proc ip*(req: Request): string = + ## IP address of the requesting client. + when useHttpBeast: + result = req.req.ip + else: + result = req.req.hostname + + let headers = req.headers + if headers.hasKey("REMOTE_ADDR"): + result = headers["REMOTE_ADDR"] + if headers.hasKey("x-forwarded-for"): + result = headers["x-forwarded-for"] + +proc params*(req: Request): Table[string, string] = + ## Parameters from the pattern and the query string. + if req.patternParams.isSome(): + result = req.patternParams.get() + else: + result = initTable[string, string]() + + when useHttpBeast: + let query = req.req.path.get("").parseUri().query + else: + let query = req.req.url.query + + try: + for key, val in cgi.decodeData(query): + result[key] = val + except CgiError: + logging.warn("Incorrect query. Got: $1" % [query]) + + let contentType = req.headers.getOrDefault("Content-Type") + if contentType.startswith("application/x-www-form-urlencoded"): + try: + parseUrlQuery(req.body, result) + except: + logging.warn("Could not parse URL query.") + +proc formData*(req: Request): MultiData = + let contentType = req.headers.getOrDefault("Content-Type") + if contentType.startsWith("multipart/form-data"): + result = parseMPFD(contentType, req.body) + +proc matches*(req: Request): array[MaxSubpatterns, string] = + req.reMatches + +proc secure*(req: Request): bool = + if req.headers.hasKey("x-forwarded-proto"): + let proto = req.headers["x-forwarded-proto"] + case proto.toLowerAscii() + of "https": + result = true + of "http": + result = false + else: + logging.warn("Unknown x-forwarded-proto ", proto) + +proc port*(req: Request): int = + if (let p = req.headers.getOrDefault("SERVER_PORT"); p != ""): + result = p.parseInt + else: + result = if req.secure: 443 else: 80 + +proc host*(req: Request): string = + req.headers.getOrDefault("HOST") + +proc appName*(req: Request): string = + ## This is set by the user in ``run``, it is + ## overriden by the "SCRIPT_NAME" scgi + ## parameter. + req.settings.appName + +proc stripAppName(path, appName: string): string = + result = path + if appname.len > 0: + var slashAppName = appName + if slashAppName[0] != '/' and path[0] == '/': + slashAppName = '/' & slashAppName + + if path.startsWith(slashAppName): + if slashAppName.len() == path.len: + return "/" + else: + return path[slashAppName.len .. path.len-1] + else: + raise newException(ValueError, + "Expected script name at beginning of path. Got path: " & + path & " script name: " & slashAppName) + +proc pathInfo*(req: Request): string = + ## This is ``.path`` without ``.appName``. + req.path.stripAppName(req.appName) + +# TODO: Can cookie keys be duplicated? +proc cookies*(req: Request): Table[string, string] = + ## Cookies from the browser. + if (let cookie = req.headers.getOrDefault("Cookie"); cookie != ""): + result = parseCookies(cookie) + else: + result = initTable[string, string]() + +#[ Protected procs ]# + +proc initRequest*(req: NativeRequest, settings: Settings): Request {.inline.} = + Request( + req: req, + settings: settings + ) + +proc getNativeReq*(req: Request): NativeRequest = + req.req + +#[ Only to be used by our route macro. ]# +proc setPatternParams*(req: var Request, p: Table[string, string]) = + req.patternParams = some(p) + +proc setReMatches*(req: var Request, r: array[MaxSubpatterns, string]) = + req.reMatches = r |