123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- # author: Ethosa
- # ----- CORE ----- #
- import asyncdispatch
- import asynchttpserver
- import macros
- # ----- SUPPORT ----- #
- import asyncfile # loadtemplate
- import strutils # startsWith, endsWith
- import strtabs
- import cookies
- import tables
- import times # for local()
- import json # urlParams
- import uri # decodeUrl
- import std/sha1 # sha1 passwords.
- import os
- import re # regex
- # ----- EXPORT -----
- export asyncdispatch
- export asynchttpserver
- export strutils
- export cookies
- export strtabs
- export json
- export uri
- export re
- type
- ServerRef* = ref object
- port*: uint16
- address*: string
- server*: AsyncHttpServer
- var AKANE_DEBUG_MODE*: bool = false ## change it with `newServer proc<#newServer,string,uint16,bool>`_
- # ---------- PRIVATE ---------- #
- proc toStr(node: JsonNode): Future[string] {.async.} =
- if node.kind == JString:
- return node.getStr
- else:
- return $node
- # ---------- PUBLIC ---------- #
- proc newServer*(address: string = "127.0.0.1",
- port: uint16 = 5000, debug: bool = false): ServerRef =
- ## Creates a new ServerRef object.
- ##
- ## Arguments:
- ## - ``address`` - server address, e.g. "127.0.0.1"
- ## - ``port`` - server port, e.g. 5000
- ## - ``debug`` - debug mode
- AKANE_DEBUG_MODE = debug
- if not existsDir("templates"):
- createDir("templates")
- if AKANE_DEBUG_MODE:
- echo "directory \"templates\" was created."
- return ServerRef(
- address: address, port: port,
- server: newAsyncHttpServer()
- )
- proc loadtemplate*(name: string, json: JsonNode = %*{}): Future[string] {.async, inline.} =
- ## Loads HTML template from `templates` folder.
- ##
- ## Arguments:
- ## - ``name`` - template's name, e.g. "index", "api", etc.
- ## - ``json`` - Json data, which replaces in the template.
- ##
- ## Replaces:
- ## - @key -> value
- ## - if @key { ... } -> ... (if value is true)
- ## - if not @key { ... } -> ... (if value is false)
- ## - for i in 0..@key { ... } -> ........., etc
- ## - @key[0] -> key[0]
- var
- file = openAsync(("templates" / name) & ".html")
- readed = await file.readAll()
- file.close()
- for key, value in json.pairs:
- # ---- regex patterns ---- #
- let
- # variable statement, e.g.: $(variable)
- variable_stmt = re("(@" & key & ")")
- # if statement, e.g.: if $(variable) {......}
- if_stmt = re("if\\s*(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
- # if not statement, e.g.: if not $(variable) {......}
- if_notstmt = re("if\\s*not\\s*(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
- # for statement, e.g.: for i in 0..$(variable) {hello, $variable[i]}
- forstmt = re(
- "for\\s*([\\S]+)\\s*in\\s*(\\d+)\\.\\.(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
- var
- matches: array[20, string]
- now = 0
- # ---- converts value to bool ---- #
- var value_bool =
- case value.kind:
- of JBool:
- value.getBool
- of JInt:
- value.getInt != 0
- of JFloat:
- value.getFloat != 0.0
- of JString:
- value.getStr.len > 0
- of JArray:
- value.len > 0
- of JObject:
- value.getFields.len > 0
- else: false
- # ---- replace ----- #
- if readed.contains(if_stmt):
- if value_bool:
- readed = readed.replacef(if_stmt, "$2")
- else:
- readed = readed.replacef(if_stmt, "")
- if readed.contains(if_notstmt):
- if value_bool:
- readed = readed.replacef(if_notstmt, "")
- else:
- readed = readed.replacef(if_notstmt, "$2")
- while readed.contains(forstmt):
- let
- (start, stop) = readed.findBounds(forstmt, matches, now)
- elem = re("(" & key & "\\[" & matches[0] & "\\])")
- var output = ""
- for i in parseInt(matches[1])..<value.len:
- output &= matches[3].replacef(elem, await value[i].toStr)
- readed = readed[0..start-1] & output & readed[stop+1..^1]
- now += stop
- readed = readed.replacef(variable_stmt, await value.toStr)
- return readed
- proc parseQuery*(request: Request): Future[JsonNode] {.async.} =
- ## Decodes query.
- ## e.g.:
- ## "a=5&b=10" -> {"a": "5", "b": "10"}
- ##
- ## This also have debug output, if AKANE_DEBUG_MODE is true.
- var data = request.url.query.split("&")
- result = %*{}
- for i in data:
- let timed = i.split("=")
- if timed.len > 1:
- result[decodeUrl(timed[0])] = %decodeUrl(timed[1])
- if AKANE_DEBUG_MODE:
- let
- now = times.local(times.getTime())
- timed_month = ord(now.month)
- month = if timed_month > 9: $timed_month else: "0" & $timed_month
- day = if now.monthday > 9: $now.monthday else: "0" & $now.monthday
- hour = if now.hour > 9: $now.hour else: "0" & $now.hour
- minute = if now.minute > 9: $now.minute else: "0" & $now.minute
- second = if now.second > 9: $now.second else: "0" & $now.second
- host =
- if request.headers.hasKey("host") and request.headers["host"].len > 1:
- request.headers["host"] & " "
- else:
- "new "
- echo(
- host, request.reqMethod,
- " at ", now.year, ".", month, ".", day,
- " ", hour, ":", minute, ":", second,
- " Request from ", request.hostname,
- " to url \"", decodeUrl(request.url.path), "\".")
- echo request
- proc password2hash*(password: string): Future[string] {.async, inline.} =
- ## Generates a sha1 from `password`.
- ##
- ## Arguments:
- ## - ``password`` - user password.
- return $secureHash(password)
- proc validatePassword*(password, hashpassword: string): Future[bool] {.async, inline.} =
- ## Validates the password and returns true, if the password is valid.
- ##
- ## Arguments:
- ## - ``password`` - got password from user input.
- ## - ``hashpassword`` - response from `password2hash proc <#password2hash,string>`_
- return secureHash(password) == parseSecureHash(hashpassword)
- proc newCookie*(server: ServerRef, key, value: string, domain = ""): HttpHeaders {.inline.} =
- ## Creates a new cookies
- let d = if domain != "": domain else: server.address
- return newHttpHeaders([("Set-Cookie", setCookie(key, value, d, noName=true))])
- macro pages*(server: ServerRef, body: untyped): untyped =
- ## This macro provides convenient page adding.
- ##
- ## `body` should be StmtList.
- ## page type can be:
- ## - ``equals``
- ## - ``startswith``
- ## - ``endswith``
- ## - ``regex``
- ## - ``notfound`` - this page uses without URL argument.
- ##
- ## When a new request to the server is received, variables are automatically created:
- ## - ``request`` - new Request.
- ## - ``url`` - matched URL.
- ## - ``equals`` - URL is request.url.path
- ## - ``startswith`` - URL is text after `startswith`.
- ## - ``endswith`` - URL is text before `endswith`.
- ## - ``regex`` - URL is matched text.
- ## - ``notfound`` - `url` param not created.
- ## - ``urlParams`` - query URL (in JSON).
- ## - ``decoded_url`` - URL always is request.url.path
- ## - ``cookies`` - StringTable of cookies.
- # ------ EXAMPLES ------ #
- runnableExamples:
- let server = newServer(debug=true)
- server.pages:
- equals("/home"):
- echo url
- echo urlParams
- await request.answer("Home")
- # You can also not write `equals("/")`:
- "/helloworld":
- await request.answer("Hello, world")
- # ------ CODE ------ #
- var
- stmtlist = newStmtList()
- notfound_declaration = false
- stmtlist.add(
- newNimNode(nnkLetSection).add( # let urlParams: JsonNode = await parseQuery(request)
- newNimNode(nnkIdentDefs).add(
- ident("urlParams"),
- ident("JsonNode"),
- newCall(
- "await",
- newCall("parseQuery", ident("request"))
- )
- ),
- newNimNode(nnkIdentDefs).add( # let decode_url: string = decodeUrl(request.url.path)
- ident("decoded_url"),
- ident("string"),
- newCall(
- "decodeUrl",
- newNimNode(nnkDotExpr).add(
- newNimNode(nnkDotExpr).add(
- ident("request"), ident("url")
- ),
- ident("path")
- )
- )
- ),
- newNimNode(nnkIdentDefs).add( # let cookies: string = parseCookies(request.headers.cookie)
- ident("cookies"),
- ident("StringTableRef"),
- newNimNode(nnkIfExpr).add(
- newNimNode(nnkElifExpr).add(
- newCall("hasKey", newNimNode(nnkDotExpr).add(ident("request"), ident("headers")), newLit("cookie")),
- newCall(
- "parseCookies",
- newCall(
- "[]",
- newNimNode(nnkDotExpr).add(
- ident("request"), ident("headers")
- ),
- newLit("cookie")
- )
- )
- ),
- newNimNode(nnkElseExpr).add(
- newCall("newStringTable", ident("modeCaseSensitive"))
- )
- )
- )
- )
- )
- stmtlist.add(newNimNode(nnkIfStmt))
- var ifstmtlist = stmtlist[1]
- for i in body: # for each page in statment list.
- let
- current = if i.len == 3: $i[0] else: "equals"
- path = if i.len == 3: i[1] else: i[0]
- slist = if i.len == 3: i[2] else: i[1]
- if (i.kind == nnkCall and
- (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and
- slist.kind == nnkStmtList):
- if current == "equals":
- slist.insert(0, # let url: string = `path`
- newNimNode(nnkLetSection).add(
- newNimNode(nnkIdentDefs).add(
- ident("url"), ident("string"), path
- )
- )
- )
- ifstmtlist.add( # decoded_url == `path`
- newNimNode(nnkElifBranch).add(
- newCall("==", path, ident("decoded_url")),
- slist
- )
- )
- elif current == "startswith":
- slist.insert(0, # let url = decoded_url[`path`.len..^1]
- newNimNode(nnkLetSection).add(
- newNimNode(nnkIdentDefs).add(
- ident("url"),
- ident("string"),
- newCall(
- "[]",
- ident("decoded_url"),
- newCall("..^", newCall("len", path), newLit(1))
- )
- )
- )
- )
- ifstmtlist.add( # decode_url.startsWith(`path`)
- newNimNode(nnkElifBranch).add(
- newCall("startsWith", ident("decoded_url"), path),
- slist
- )
- )
- elif current == "endswith":
- slist.insert(0, # let url: string = decoded_url[0..^`path`.len]
- newNimNode(nnkLetSection).add(
- newNimNode(nnkIdentDefs).add(
- ident("url"),
- ident("string"),
- newCall(
- "[]",
- ident("decoded_url"),
- newCall(
- "..^", newLit(0), newCall("+", newLit(1), newCall("len", path))
- )
- )
- )
- )
- )
- ifstmtlist.add( # decode_url.endsWith(`path`)
- newNimNode(nnkElifBranch).add(
- newCall("endsWith", ident("decoded_url"), path),
- slist
- )
- )
- elif current == "regex":
- slist.insert(0, # discard match(decoded_url, `path`, url)
- newNimNode(nnkDiscardStmt).add(
- newCall("match", ident("decoded_url"), path, ident("url"))
- )
- )
- slist.insert(0, # var url: array[20, string]
- newNimNode(nnkVarSection).add(
- newNimNode(nnkIdentDefs).add(
- ident("url"),
- newNimNode(nnkBracketExpr).add(
- ident("array"), newLit(20), ident("string")
- ),
- newEmptyNode()
- )
- ))
- ifstmtlist.add( # decode_url.match(`path`)
- newNimNode(nnkElifBranch).add(
- newCall("match", ident("decoded_url"), path),
- slist))
- elif current == "notfound":
- notfound_declaration = true
- ifstmtlist.add(newNimNode(nnkElse).add(slist))
- if not notfound_declaration:
- ifstmtlist.add(
- newNimNode(nnkElse).add(
- newCall( # await request.respond(Http404, "Not found")
- "await",
- newCall("respond", ident("request"), ident("Http404"), newLit("Not found"))
- )
- )
- )
- result = newNimNode(nnkProcDef).add(
- ident("receivepages"), # procedure name.
- newEmptyNode(), # for template and macros
- newEmptyNode(), # generics
- newNimNode(nnkFormalParams).add( # proc params
- newEmptyNode(), # return type
- newNimNode(nnkIdentDefs).add( # param
- ident("request"), # param name
- ident("Request"), # param type
- newEmptyNode() # param default value
- )
- ),
- newNimNode(nnkPragma).add( # pragma declaration
- ident("async"),
- ident("gcsafe")
- ),
- newEmptyNode(),
- stmtlist)
- macro answer*(request, message: untyped, http_code = Http200,
- headers: HttpHeaders = newHttpHeaders()): untyped =
- ## Responds from server with utf-8.
- ##
- ## Translates to:
- ## request.respond(Http200, "<head><meta charset='utf-8'></head>" & message)
- result = newCall(
- "respond",
- request,
- http_code,
- newCall("&", newLit("<head><meta charset='utf-8'></head>"), message),
- headers
- )
- macro error*(request, message: untyped, http_code = Http404,
- headers: HttpHeaders = newHttpHeaders()): untyped =
- ## Responds from server with utf-8.
- ##
- ## Translates to:
- ## request.respond(Http404, "<head><meta charset='utf-8'></head>" & message)
- result = newCall(
- "respond",
- request,
- http_code,
- newCall("&", newLit("<head><meta charset='utf-8'></head>"), message),
- headers
- )
- macro sendJson*(request, message: untyped, http_code = Http200): untyped =
- ## Sends JsonNode with "Content-Type": "application/json" in headers.
- ##
- ## Translates to:
- ## request.respond(
- ## Http200,
- ## $message,
- ## newHttpHeaders([("Content-Type","application/json")]))
- result = newCall(
- "respond",
- request,
- http_code,
- newCall("$", message),
- newCall(
- "newHttpHeaders",
- newNimNode(nnkBracket).add(
- newNimNode(nnkPar).add(
- newLit("Content-Type"),
- newLit("application/json")
- )
- )
- )
- )
- macro start*(server: ServerRef): untyped =
- ## Starts server.
- result = quote do:
- if AKANE_DEBUG_MODE:
- echo "Server starts on http://", `server`.address, ":", `server`.port
- waitFor `server`.server.serve(Port(`server`.port), receivepages, `server`.address)
|