akane.nim 18 KB

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