akane.nim 15 KB

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