akane.nim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. # author: Ethosa
  2. # ----- CORE ----- #
  3. import asyncdispatch
  4. import asynchttpserver
  5. # ----- SUPPORT ----- #
  6. import asyncfile # loadtemplate
  7. import strutils # startsWith, endsWith
  8. import tables
  9. import macros
  10. import times
  11. import json # urlParams
  12. import uri # decodeUrl
  13. import std/sha1 # sha1 passwords.
  14. import os
  15. import re # regex
  16. # ----- EXPORT -----
  17. export asyncdispatch
  18. export asynchttpserver
  19. export strutils
  20. export json
  21. export uri
  22. export re
  23. type
  24. ServerRef* = ref object
  25. port*: uint16
  26. address*: string
  27. server*: AsyncHttpServer
  28. var AKANE_DEBUG_MODE*: bool = false ## change it with `newServer proc<#newServer,string,uint16,bool>`_
  29. # ---------- PRIVATE ---------- #
  30. proc toStr(node: JsonNode): Future[string] {.async.} =
  31. if node.kind == JString:
  32. return node.getStr
  33. else:
  34. return $node
  35. # ---------- PUBLIC ---------- #
  36. proc newServer*(address: string = "127.0.0.1",
  37. port: uint16 = 5000, debug: bool = false): ServerRef =
  38. ## Creates a new ServerRef object.
  39. ##
  40. ## Arguments:
  41. ## - ``address`` - server address, e.g. "127.0.0.1"
  42. ## - ``port`` - server port, e.g. 5000
  43. ## - ``debug`` - debug mode
  44. if not existsDir("templates"):
  45. createDir("templates")
  46. AKANE_DEBUG_MODE = debug
  47. return ServerRef(
  48. address: address, port: port,
  49. server: newAsyncHttpServer()
  50. )
  51. proc loadtemplate*(name: string, json: JsonNode = %*{}): Future[string] {.async, inline.} =
  52. ## Loads HTML template from `templates` folder.
  53. ##
  54. ## Arguments:
  55. ## - ``name`` - template's name, e.g. "index", "api", etc.
  56. ## - ``json`` - Json data, which replaces in the template.
  57. ##
  58. ## Replaces:
  59. ## - $(key) -> value
  60. ## - if $(key) { ... } -> ... (if value is true)
  61. ## - if not $(key) { ... } -> ... (if value is false)
  62. ## - for i in 0..$(key) { ... } -> ........., etc
  63. var
  64. file = openAsync(("templates" / name) & ".html")
  65. readed = await file.readAll()
  66. file.close()
  67. for key, value in json.pairs:
  68. # ---- regex patterns ---- #
  69. let
  70. # variable statement, e.g.: $(variable)
  71. variable_stmt = re("(\\$\\s*\\(" & key & "\\))")
  72. # if statement, e.g.: if $(variable) {......}
  73. if_stmt = re("if\\s*(\\$\\s*\\(" & key & "\\))\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  74. # if not statement, e.g.: if not $(variable) {......}
  75. if_notstmt = re("if\\s*not\\s*(\\$\\s*\\(" & key & "\\))\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  76. # for statement, e.g.: for i in 0..$(variable) {hello, $variable[i]}
  77. forstmt = re(
  78. "for\\s*([\\S]+)\\s*in\\s*(\\d+)\\.\\.(\\$\\s*\\(" & key & "\\))\\s*\\{\\s*([\\s\\S]+?)\\s*\\}")
  79. var
  80. matches: array[20, string]
  81. now = 0
  82. # ---- converts value to bool ---- #
  83. var value_bool = false
  84. case value.kind:
  85. of JBool:
  86. if value.getBool:
  87. value_bool = true
  88. of JInt:
  89. if value.getInt != 0:
  90. value_bool = true
  91. of JFloat:
  92. if value.getFloat != 0.0:
  93. value_bool = true
  94. of JString:
  95. if value.getStr.len > 0:
  96. value_bool = true
  97. of JArray:
  98. if value.len > 0:
  99. value_bool = true
  100. of JObject:
  101. if value.getFields.len > 0:
  102. value_bool = true
  103. else: discard
  104. # ---- replace ----- #
  105. if readed.contains(if_stmt):
  106. if value_bool:
  107. readed = readed.replacef(if_stmt, "$2")
  108. else:
  109. readed = readed.replacef(if_stmt, "")
  110. if readed.contains(if_notstmt):
  111. if value_bool:
  112. readed = readed.replacef(if_notstmt, "")
  113. else:
  114. readed = readed.replacef(if_notstmt, "$2")
  115. while readed.contains(forstmt):
  116. let
  117. (start, stop) = readed.findBounds(forstmt, matches, now)
  118. elem = re("(\\$" & key & "\\[" & matches[0] & "\\])")
  119. var output = ""
  120. for i in parseInt(matches[1])..<value.len:
  121. output &= matches[3].replacef(elem, await value[i].toStr)
  122. readed = readed[0..start-1] & output & readed[stop+1..^1]
  123. now += stop
  124. readed = readed.replacef(variable_stmt, await value.toStr)
  125. return readed
  126. proc parseQuery*(request: Request): Future[JsonNode] {.async.} =
  127. ## Decodes query.
  128. ## e.g.:
  129. ## "a=5&b=10" -> {"a": "5", "b": "10"}
  130. ##
  131. ## This also have debug output, if AKANE_DEBUG_MODE is true.
  132. var data = request.url.query.split("&")
  133. result = %*{}
  134. for i in data:
  135. let timed = i.split("=")
  136. if timed.len > 1:
  137. result[decodeUrl(timed[0])] = %decodeUrl(timed[1])
  138. if AKANE_DEBUG_MODE:
  139. let
  140. now = times.local(times.getTime())
  141. timed_month = ord(now.month)
  142. month = if timed_month > 9: $timed_month else: "0" & $timed_month
  143. day = if now.monthday > 9: $now.monthday else: "0" & $now.monthday
  144. hour = if now.hour > 9: $now.hour else: "0" & $now.hour
  145. minute = if now.minute > 9: $now.minute else: "0" & $now.minute
  146. second = if now.second > 9: $now.second else: "0" & $now.second
  147. host =
  148. if request.headers.hasKey("host") and request.headers["host"].len > 1:
  149. request.headers["host"] & " "
  150. else:
  151. "new "
  152. echo(
  153. host, request.reqMethod,
  154. " at ", now.year, ".", month, ".", day,
  155. " ", hour, ":", minute, ":", second,
  156. " Request from ", request.hostname,
  157. " to url \"", decodeUrl(request.url.path), "\".")
  158. proc password2hash*(password: string): Future[string] {.async, inline.} =
  159. ## Generates a sha1 from `password`.
  160. ##
  161. ## Arguments:
  162. ## - ``password`` - user password.
  163. return $secureHash(password)
  164. proc validatePassword*(password, hashpassword: string): Future[bool] {.async, inline.} =
  165. ## Validates the password and returns true, if the password is valid.
  166. ##
  167. ## Arguments:
  168. ## - ``password`` - got password from user input.
  169. ## - ``hashpassword`` - response from `password2hash proc <#password2hash,string>`_
  170. return secureHash(password) == parseSecureHash(hashpassword)
  171. macro pages*(server: ServerRef, body: untyped): untyped =
  172. ## This macro provides convenient page adding.
  173. ##
  174. ## `body` should be StmtList.
  175. ## page type can be:
  176. ## - ``equals``
  177. ## - ``startswith``
  178. ## - ``endswith``
  179. ## - ``regex``
  180. ## - ``notfound`` - this page uses without URL argument.
  181. # ------ EXAMPLES ------ #
  182. runnableExamples:
  183. let server = newServer(debug=true)
  184. server.pages:
  185. equals("/home"):
  186. echo url
  187. echo urlParams
  188. await request.answer("Home")
  189. # You can also not write `equals("/")`:
  190. "/helloworld":
  191. await request.answer("Hello, world")
  192. # ------ CODE ------ #
  193. var
  194. stmtlist = newStmtList()
  195. notfound_declaration = false
  196. stmtlist.add(
  197. newNimNode(nnkLetSection).add( # let urlParams: JsonNode = await parseQuery(request)
  198. newNimNode(nnkIdentDefs).add(
  199. ident("urlParams"),
  200. ident("JsonNode"),
  201. newCall(
  202. "await",
  203. newCall(
  204. "parseQuery",
  205. ident("request")
  206. )
  207. )
  208. ),
  209. newNimNode(nnkIdentDefs).add( # let decode_url: string = decodeUrl(request.url.path)
  210. ident("decoded_url"),
  211. ident("string"),
  212. newCall(
  213. "decodeUrl",
  214. newNimNode(nnkDotExpr).add(
  215. newNimNode(nnkDotExpr).add(
  216. ident("request"), ident("url")
  217. ),
  218. ident("path")
  219. )
  220. )
  221. )
  222. )
  223. )
  224. stmtlist.add(newNimNode(nnkIfStmt))
  225. var ifstmtlist = stmtlist[1]
  226. for i in body: # for each page in statment list.
  227. let
  228. current = if i.len == 3: $i[0] else: "equals"
  229. path = if i.len == 3: i[1] else: i[0]
  230. slist = if i.len == 3: i[2] else: i[1]
  231. if (i.kind == nnkCall and
  232. (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and
  233. slist.kind == nnkStmtList):
  234. if current == "equals":
  235. slist.insert(0, # let url: string = `path`
  236. newNimNode(nnkLetSection).add(
  237. newNimNode(nnkIdentDefs).add(
  238. ident("url"),
  239. ident("string"),
  240. path
  241. )
  242. )
  243. )
  244. ifstmtlist.add( # decoded_url == `path`
  245. newNimNode(nnkElifBranch).add(
  246. newCall("==", path, ident("decoded_url")),
  247. slist))
  248. elif current == "startswith":
  249. slist.insert(0, # let url = decoded_url[`path`.len..^1]
  250. newNimNode(nnkLetSection).add(
  251. newNimNode(nnkIdentDefs).add(
  252. ident("url"),
  253. ident("string"),
  254. newCall(
  255. "[]",
  256. ident("decoded_url"),
  257. newCall(
  258. "..^",
  259. newCall("len", path),
  260. newLit(1))
  261. )
  262. )
  263. )
  264. )
  265. ifstmtlist.add( # decode_url.startsWith(`path`)
  266. newNimNode(nnkElifBranch).add(
  267. newCall(
  268. "startsWith",
  269. ident("decoded_url"),
  270. path),
  271. slist))
  272. elif current == "endswith":
  273. slist.insert(0, # let url: string = decoded_url[0..^`path`.len]
  274. newNimNode(nnkLetSection).add(
  275. newNimNode(nnkIdentDefs).add(
  276. ident("url"),
  277. ident("string"),
  278. newCall(
  279. "[]",
  280. ident("decoded_url"),
  281. newCall(
  282. "..^",
  283. newLit(0),
  284. newCall("+", newLit(1), newCall("len", path))
  285. )
  286. )
  287. )
  288. )
  289. )
  290. ifstmtlist.add( # decode_url.endsWith(`path`)
  291. newNimNode(nnkElifBranch).add(
  292. newCall(
  293. "endsWith",
  294. ident("decoded_url"),
  295. path),
  296. slist))
  297. elif current == "regex":
  298. slist.insert(0, # discard match(decoded_url, `path`, url)
  299. newNimNode(nnkDiscardStmt).add(
  300. newCall("match", ident("decoded_url"), path, ident("url"))
  301. )
  302. )
  303. slist.insert(0, # var url: array[20, string]
  304. newNimNode(nnkVarSection).add(
  305. newNimNode(nnkIdentDefs).add(
  306. ident("url"),
  307. newNimNode(nnkBracketExpr).add(
  308. ident("array"), newLit(20), ident("string")
  309. ),
  310. newEmptyNode()
  311. )
  312. ))
  313. ifstmtlist.add( # decode_url.match(`path`)
  314. newNimNode(nnkElifBranch).add(
  315. newCall("match", ident("decoded_url"), path),
  316. slist))
  317. elif current == "notfound":
  318. notfound_declaration = true
  319. ifstmtlist.add(newNimNode(nnkElse).add(slist))
  320. if not notfound_declaration:
  321. ifstmtlist.add(
  322. newNimNode(nnkElse).add(
  323. newCall( # await request.respond(Http404, "Not found")
  324. "await",
  325. newCall(
  326. "respond",
  327. ident("request"),
  328. ident("Http404"),
  329. newLit("Not found"))
  330. )
  331. )
  332. )
  333. result = newNimNode(nnkProcDef).add(
  334. ident("receivepages"), # procedure name.
  335. newEmptyNode(), # for template and macros
  336. newEmptyNode(), # generics
  337. newNimNode(nnkFormalParams).add( # proc params
  338. newEmptyNode(), # return type
  339. newNimNode(nnkIdentDefs).add( # param
  340. ident("request"), # param name
  341. ident("Request"), # param type
  342. newEmptyNode() # param default value
  343. )
  344. ),
  345. newNimNode(nnkPragma).add( # pragma declaration
  346. ident("async"),
  347. ident("gcsafe")
  348. ),
  349. newEmptyNode(),
  350. stmtlist)
  351. macro answer*(request, message: untyped, http_code = Http200): untyped =
  352. ## Responds from server with utf-8.
  353. ##
  354. ## Translates to:
  355. ## request.respond(Http200, "<head><meta charset='utf-8'></head>" & message)
  356. result = newCall(
  357. "respond",
  358. request,
  359. http_code,
  360. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message)
  361. )
  362. macro error*(request, message: untyped, http_code = Http404): untyped =
  363. ## Responds from server with utf-8.
  364. ##
  365. ## Translates to:
  366. ## request.respond(Http404, "<head><meta charset='utf-8'></head>" & message)
  367. result = newCall(
  368. "respond",
  369. request,
  370. http_code,
  371. newCall("&", newLit("<head><meta charset='utf-8'></head>"), message)
  372. )
  373. macro sendJson*(request, message: untyped, http_code = Http200): untyped =
  374. ## Sends JsonNode with "Content-Type": "application/json" in headers.
  375. ##
  376. ## Translates to:
  377. ## request.respond(
  378. ## Http200,
  379. ## $message,
  380. ## newHttpHeaders([("Content-Type","application/json")]))
  381. result = newCall(
  382. "respond",
  383. request,
  384. http_code,
  385. newCall("$", message),
  386. newCall(
  387. "newHttpHeaders",
  388. newNimNode(nnkBracket).add(
  389. newNimNode(nnkPar).add(
  390. newLit("Content-Type"),
  391. newLit("application/json")
  392. )
  393. )
  394. )
  395. )
  396. macro start*(server: ServerRef): untyped =
  397. ## Starts server.
  398. result = quote do:
  399. if AKANE_DEBUG_MODE:
  400. echo "Server starts on http://", `server`.address, ":", `server`.port
  401. waitFor `server`.server.serve(Port(`server`.port), receivepages, `server`.address)