akane.nim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. # author: Ethosa
  2. # ----- CORE ----- #
  3. import asyncdispatch
  4. import asynchttpserver
  5. import macros
  6. # ----- SUPPORT ----- #
  7. import asyncfile # loadtemplate
  8. import strutils # startsWith, endsWith
  9. import strtabs
  10. import cookies
  11. import tables
  12. import json # urlParams
  13. import uri # decodeUrl
  14. import std/sha1 # sha1 passwords.
  15. import os
  16. import re # regex
  17. # ----- EXPORT -----
  18. export asyncdispatch
  19. export asynchttpserver
  20. export strutils
  21. export cookies
  22. export strtabs
  23. export json
  24. export uri
  25. export re
  26. when defined(debug):
  27. import logging
  28. var console_logger = newConsoleLogger(fmtStr="[$time]::$levelname - ")
  29. addHandler(console_logger)
  30. when not defined(android):
  31. var file_logger = newFileLogger("logs.log", fmtStr="[$date at $time]::$levelname - ")
  32. addHandler(file_logger)
  33. info("Compiled in debug mode.")
  34. ## ## Simple usage
  35. ## .. code-block:: nim
  36. ##
  37. ## let my_server = newServer("127.0.0.1", 8080) # starts server at https://127.0.0.1:8080
  38. ##
  39. ## my_sever.pages:
  40. ## "/":
  41. ## echo "Index page"
  42. ## await request.answer("Hello, world!")
  43. ## notfound:
  44. ## echo "oops :("
  45. ## await request.error("404 Page not found.")
  46. type
  47. ServerRef* = ref object
  48. port*: uint16
  49. address*: string
  50. server*: AsyncHttpServer
  51. # ---------- PRIVATE ---------- #
  52. proc toStr(node: JsonNode): Future[string] {.async.} =
  53. if node.kind == JString:
  54. return node.getStr
  55. else:
  56. return $node
  57. # ---------- PUBLIC ---------- #
  58. proc newServer*(address: string = "127.0.0.1", port: uint16 = 5000): ServerRef =
  59. ## Creates a new ServerRef object.
  60. ##
  61. ## Arguments:
  62. ## - `address` - server address, e.g. "127.0.0.1"
  63. ## - `port` - server port, e.g. 5000
  64. ##
  65. ## ## Example
  66. ## .. code-block:: nim
  67. ##
  68. ## let server = newServer("127.0.0.1", 5000)
  69. if not existsDir("templates"):
  70. createDir("templates")
  71. when defined(debug):
  72. debug("directory \"templates\" was created.")
  73. ServerRef(address: address, port: port, server: newAsyncHttpServer())
  74. proc loadtemplate*(name: string, json: JsonNode = %*{}): Future[string] {.async, inline.} =
  75. ## Loads HTML template from `templates` folder.
  76. ##
  77. ## Arguments:
  78. ## - `name` - template's name, e.g. "index", "api", etc.
  79. ## - `json` - Json data, which replaces in the template.
  80. ##
  81. ## Replaces:
  82. ## - @key -> value
  83. ## - if @key { ... } -> ... (if value is true)
  84. ## - if not @key { ... } -> ... (if value is false)
  85. ## - for i in 0..@key { ... } -> ........., etc
  86. ## - @key[0] -> key[0]
  87. ##
  88. ## ## Example
  89. ## .. code-block:: nim
  90. ##
  91. ## let template = loadtemplate("index.html", %*{"a": 5})
  92. var
  93. file = openAsync(("templates" / name) & ".html")
  94. readed = await file.readAll()
  95. file.close()
  96. for key, value in json.pairs:
  97. # ---- regex patterns ---- #
  98. let
  99. # variable statement, e.g.: $(variable)
  100. variable_stmt = re("(@" & key & ")")
  101. # if statement, e.g.: if $(variable) {......}
  102. if_stmt = re("if\\s*(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  103. # if not statement, e.g.: if not $(variable) {......}
  104. if_notstmt = re("if\\s*not\\s*(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  105. # for statement, e.g.: for i in 0..$(variable) {hello, $variable[i]}
  106. forstmt = re(
  107. "for\\s*([\\S]+)\\s*in\\s*(\\d+)\\.\\.(@" & key & ")\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  108. var
  109. matches: array[20, string]
  110. now = 0
  111. # ---- converts value to bool ---- #
  112. var value_bool =
  113. case value.kind:
  114. of JBool:
  115. value.getBool
  116. of JInt:
  117. value.getInt != 0
  118. of JFloat:
  119. value.getFloat != 0.0
  120. of JString:
  121. value.getStr.len > 0
  122. of JArray:
  123. value.len > 0
  124. of JObject:
  125. value.getFields.len > 0
  126. else: false
  127. # ---- replace ----- #
  128. if readed.contains(if_stmt):
  129. if value_bool:
  130. readed = readed.replacef(if_stmt, "$2")
  131. else:
  132. readed = readed.replacef(if_stmt, "")
  133. if readed.contains(if_notstmt):
  134. if value_bool:
  135. readed = readed.replacef(if_notstmt, "")
  136. else:
  137. readed = readed.replacef(if_notstmt, "$2")
  138. while readed.contains(forstmt):
  139. let
  140. (start, stop) = readed.findBounds(forstmt, matches, now)
  141. elem = re("(" & key & "\\[" & matches[0] & "\\])")
  142. var output = ""
  143. for i in parseInt(matches[1])..<value.len:
  144. output &= matches[3].replacef(elem, await value[i].toStr)
  145. readed = readed[0..start-1] & output & readed[stop+1..^1]
  146. now += stop
  147. readed = readed.replacef(variable_stmt, await value.toStr)
  148. return readed
  149. proc parseQuery*(request: Request): Future[JsonNode] {.async.} =
  150. ## Decodes query.
  151. ## e.g.:
  152. ## "a=5&b=10" -> {"a": "5", "b": "10"}
  153. ##
  154. ## This also have debug output, if compiled in debug mode.
  155. var data = request.url.query.split("&")
  156. result = %*{}
  157. for i in data:
  158. let timed = i.split("=")
  159. if timed.len > 1:
  160. result[decodeUrl(timed[0])] = %decodeUrl(timed[1])
  161. when defined(debug):
  162. let host =
  163. if request.headers.hasKey("host") and request.headers["host"].len > 1:
  164. request.headers["host"] & " "
  165. else:
  166. "new "
  167. debug(host, request.reqMethod, " Request from ", request.hostname, " to url \"", decodeUrl(request.url.path), "\".")
  168. debug(request)
  169. proc password2hash*(password: string): Future[string] {.async, inline.} =
  170. ## Generates a sha1 from `password`.
  171. ##
  172. ## Arguments:
  173. ## - `password` is an user password.
  174. return $secureHash(password)
  175. proc validatePassword*(password, hashpassword: string): Future[bool] {.async, inline.} =
  176. ## Validates the password and returns true, if the password is valid.
  177. ##
  178. ## Arguments:
  179. ## - `password` is a got password from user input.
  180. ## - `hashpassword` is a response from `password2hash proc <#password2hash,string>`_
  181. return secureHash(password) == parseSecureHash(hashpassword)
  182. proc newCookie*(server: ServerRef, key, value: string, domain = ""): HttpHeaders {.inline.} =
  183. ## Creates a new cookies
  184. ##
  185. ## Arguments:
  186. ## - `key` is a cookie key.
  187. ## - `value` is a new cookie value.
  188. ## - `domain` is a cookie doomain.
  189. let d = if domain != "": domain else: server.address
  190. return newHttpHeaders([("Set-Cookie", setCookie(key, value, d, noName=true))])
  191. macro pages*(server: ServerRef, body: untyped): untyped =
  192. ## This macro provides convenient page adding.
  193. ##
  194. ## `body` should be StmtList.
  195. ## page type can be:
  196. ## - `equals`
  197. ## - `startswith`
  198. ## - `endswith`
  199. ## - `regex` - match url via regex.
  200. ## - `notfound` - this page uses without URL argument.
  201. ##
  202. ## When a new request to the server is received, variables are automatically created:
  203. ## - `request` - new Request.
  204. ## - `url` - matched URL.
  205. ## - `equals` - URL is request.url.path
  206. ## - `startswith` - URL is text after `startswith`.
  207. ## - `endswith` - URL is text before `endswith`.
  208. ## - `regex` - URL is matched text.
  209. ## - `notfound` - `url` param not created.
  210. ## - `urlParams` - query URL (in JSON).
  211. ## - `decoded_url` - URL always is request.url.path
  212. ## - `cookies` - StringTable of cookies.
  213. # ------ EXAMPLES ------ #
  214. runnableExamples:
  215. let server = newServer()
  216. server.pages:
  217. equals("/home"):
  218. echo url
  219. echo urlParams
  220. await request.answer("Home")
  221. # You can also not write `equals("/")`:
  222. "/helloworld":
  223. await request.answer("Hello, world")
  224. # ------ CODE ------ #
  225. var
  226. stmtlist = newStmtList()
  227. notfound_declaration = false
  228. stmtlist.add(
  229. newNimNode(nnkLetSection).add( # let urlParams: JsonNode = await parseQuery(request)
  230. newNimNode(nnkIdentDefs).add(
  231. ident("urlParams"),
  232. ident("JsonNode"),
  233. newCall(
  234. "await",
  235. newCall("parseQuery", ident("request"))
  236. )
  237. ),
  238. newNimNode(nnkIdentDefs).add( # let decode_url: string = decodeUrl(request.url.path)
  239. ident("decoded_url"),
  240. ident("string"),
  241. newCall(
  242. "decodeUrl",
  243. newNimNode(nnkDotExpr).add(
  244. newNimNode(nnkDotExpr).add(
  245. ident("request"), ident("url")
  246. ),
  247. ident("path")
  248. )
  249. )
  250. ),
  251. newNimNode(nnkIdentDefs).add( # let cookies: string = parseCookies(request.headers.cookie)
  252. ident("cookies"),
  253. ident("StringTableRef"),
  254. newNimNode(nnkIfExpr).add(
  255. newNimNode(nnkElifExpr).add(
  256. newCall("hasKey", newNimNode(nnkDotExpr).add(ident("request"), ident("headers")), newLit("cookie")),
  257. newCall(
  258. "parseCookies",
  259. newCall(
  260. "[]",
  261. newNimNode(nnkDotExpr).add(
  262. ident("request"), ident("headers")
  263. ),
  264. newLit("cookie")
  265. )
  266. )
  267. ),
  268. newNimNode(nnkElseExpr).add(
  269. newCall("newStringTable", ident("modeCaseSensitive"))
  270. )
  271. )
  272. )
  273. )
  274. )
  275. stmtlist.add(newNimNode(nnkIfStmt))
  276. var ifstmtlist = stmtlist[1]
  277. for i in body: # for each page in statment list.
  278. let
  279. current = if i.len == 3: $i[0] else: "equals"
  280. path = if i.len == 3: i[1] else: i[0]
  281. slist = if i.len == 3: i[2] else: i[1]
  282. if (i.kind == nnkCall and
  283. (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and
  284. slist.kind == nnkStmtList):
  285. case current
  286. of "equals":
  287. slist.insert(0, # let url: string = `path`
  288. newNimNode(nnkLetSection).add(
  289. newNimNode(nnkIdentDefs).add(
  290. ident("url"), ident("string"), path
  291. )
  292. )
  293. )
  294. ifstmtlist.add( # decoded_url == `path`
  295. newNimNode(nnkElifBranch).add(
  296. newCall("==", path, ident("decoded_url")),
  297. slist
  298. )
  299. )
  300. of "startswith":
  301. slist.insert(0, # let url = decoded_url[`path`.len..^1]
  302. newNimNode(nnkLetSection).add(
  303. newNimNode(nnkIdentDefs).add(
  304. ident("url"),
  305. ident("string"),
  306. newCall(
  307. "[]",
  308. ident("decoded_url"),
  309. newCall("..^", newCall("len", path), newLit(1))
  310. )
  311. )
  312. )
  313. )
  314. ifstmtlist.add( # decode_url.startsWith(`path`)
  315. newNimNode(nnkElifBranch).add(
  316. newCall("startsWith", ident("decoded_url"), path),
  317. slist
  318. )
  319. )
  320. of "endswith":
  321. slist.insert(0, # let url: string = decoded_url[0..^`path`.len]
  322. newNimNode(nnkLetSection).add(
  323. newNimNode(nnkIdentDefs).add(
  324. ident("url"),
  325. ident("string"),
  326. newCall(
  327. "[]",
  328. ident("decoded_url"),
  329. newCall(
  330. "..^", newLit(0), newCall("+", newLit(1), newCall("len", path))
  331. )
  332. )
  333. )
  334. )
  335. )
  336. ifstmtlist.add( # decode_url.endsWith(`path`)
  337. newNimNode(nnkElifBranch).add(
  338. newCall("endsWith", ident("decoded_url"), path),
  339. slist
  340. )
  341. )
  342. of "regex":
  343. slist.insert(0, # discard match(decoded_url, `path`, url)
  344. newNimNode(nnkDiscardStmt).add(
  345. newCall("match", ident("decoded_url"), path, ident("url"))
  346. )
  347. )
  348. slist.insert(0, # var url: array[20, string]
  349. newNimNode(nnkVarSection).add(
  350. newNimNode(nnkIdentDefs).add(
  351. ident("url"),
  352. newNimNode(nnkBracketExpr).add(
  353. ident("array"), newLit(20), ident("string")
  354. ),
  355. newEmptyNode()
  356. )
  357. ))
  358. ifstmtlist.add( # decode_url.match(`path`)
  359. newNimNode(nnkElifBranch).add(
  360. newCall("match", ident("decoded_url"), path),
  361. slist))
  362. of "notfound":
  363. notfound_declaration = true
  364. ifstmtlist.add(newNimNode(nnkElse).add(slist))
  365. else:
  366. discard
  367. if not notfound_declaration:
  368. ifstmtlist.add(
  369. newNimNode(nnkElse).add(
  370. newCall( # await request.respond(Http404, "Not found")
  371. "await",
  372. newCall("respond", ident("request"), ident("Http404"), newLit("Not found"))
  373. )
  374. )
  375. )
  376. result = newNimNode(nnkProcDef).add(
  377. ident("receivepages"), # procedure name.
  378. newEmptyNode(), # for template and macros
  379. newEmptyNode(), # generics
  380. newNimNode(nnkFormalParams).add( # proc params
  381. newEmptyNode(), # return type
  382. newNimNode(nnkIdentDefs).add( # param
  383. ident("request"), # param name
  384. ident("Request"), # param type
  385. newEmptyNode() # param default value
  386. )
  387. ),
  388. newNimNode(nnkPragma).add( # pragma declaration
  389. ident("async"),
  390. ident("gcsafe")
  391. ),
  392. newEmptyNode(),
  393. stmtlist)
  394. macro send*(request, message: untyped, http_code = Http200,
  395. headers: HttpHeaders = newHttpHeaders()): untyped =
  396. ## Responds from server with utf-8.
  397. ## Note: Content-length does not send automatically.
  398. ##
  399. ## Translates to
  400. ##
  401. ## .. code-block:: nim
  402. ##
  403. ## request.respond(Http200, $message, headers)
  404. ##
  405. ## ## Example
  406. ## .. code-block:: nim
  407. ##
  408. ## await request.send("hello!")
  409. result = newCall("respond", request, http_code, newCall("$", message), headers)
  410. macro answer*(request, message: untyped, http_code = Http200,
  411. headers: HttpHeaders = newHttpHeaders()): untyped =
  412. ## Responds from server with utf-8.
  413. ## Note: Content-length does not send automatically.
  414. ##
  415. ## Translates to
  416. ##
  417. ## .. code-block:: nim
  418. ##
  419. ## request.respond(Http200, "<head><meta charset='utf-8'></head>" & message, headers)
  420. ##
  421. ## ## Example
  422. ## .. code-block:: nim
  423. ##
  424. ## await request.answer("hello!")
  425. result = newCall(
  426. "respond",
  427. request,
  428. http_code,
  429. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message),
  430. headers
  431. )
  432. macro error*(request, message: untyped, http_code = Http404,
  433. headers: HttpHeaders = newHttpHeaders()): untyped =
  434. ## Responds from server with utf-8.
  435. ## Note: Content-length not automatically sends.
  436. ##
  437. ## Translates to
  438. ##
  439. ## .. code-block:: nim
  440. ##
  441. ## request.respond(Http404, "<head><meta charset='utf-8'></head>" & message)
  442. ##
  443. ## ## Example
  444. ## .. code-block:: nim
  445. ##
  446. ## await request.error("Oops! :(")
  447. result = newCall(
  448. "respond",
  449. request,
  450. http_code,
  451. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message),
  452. headers
  453. )
  454. macro sendJson*(request, message: untyped, http_code = Http200): untyped =
  455. ## Sends JsonNode with "Content-Type": "application/json" in headers.
  456. ## Note: Content-length does send automatically.
  457. ##
  458. ## Translates to
  459. ##
  460. ## .. code-block:: nim
  461. ##
  462. ## request.respond(Http200, $message, newHttpHeaders([("Content-Type","application/json")]))
  463. ##
  464. ## ## Example
  465. ## .. code-block:: nim
  466. ##
  467. ## await request.sendJson(%*{"response": "error", "msg": "oops :("})
  468. quote do:
  469. `request`.respond(
  470. `http_code`, $`message`, newHttpHeaders([
  471. ("Content-Type","application/json"),
  472. ("Content-length", len($`message`))
  473. ]))
  474. macro sendPlaintext*(request, message: untyped, http_code = Http200): untyped =
  475. ## Sends JsonNode with "Content-Type": "application/json" in headers.
  476. ## Note: Content-length does send automatically.
  477. ##
  478. ## Translates to
  479. ##
  480. ## .. code-block:: nim
  481. ##
  482. ## request.respond(Http200, $message, newHttpHeaders([("Content-Type","plain/text")]))
  483. ##
  484. ## ## Example
  485. ## .. code-block:: nim
  486. ##
  487. ## await request.sendPlaintext(%*{"response": "error", "msg": "oops :("})
  488. quote do:
  489. `request`.respond(`http_code`, $`message`, newHttpHeaders([
  490. ("Content-Type","plain/text"),
  491. ("Content-length", len($`message`))
  492. ]))
  493. macro start*(server: ServerRef): untyped =
  494. ## Starts server.
  495. ##
  496. ## ## Example
  497. ## .. code-block:: nim
  498. ##
  499. ## let server = newServer()
  500. ## server.start()
  501. result = quote do:
  502. when defined(debug):
  503. debug("Server starts on http://", `server`.address, ":", `server`.port)
  504. waitFor `server`.server.serve(Port(`server`.port), receivepages, `server`.address)