akane.nim 16 KB

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