akane.nim 12 KB

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