├── LICENSE ├── README.md ├── examples ├── example_1_hello_world │ └── app.nim ├── example_2 │ ├── app.nim │ ├── public │ │ ├── images │ │ │ ├── kiira.jpg │ │ │ └── road.jpg │ │ └── videos │ │ │ └── burger.webm │ └── templates │ │ ├── index.html │ │ └── layout.html ├── example_3_domains │ ├── app.nim │ └── domains │ │ ├── api.nim │ │ └── main.nim ├── example_4_host_subdomain_router │ └── server.nim ├── example_5_withData │ └── app.nim ├── example_6_fetch │ └── app.nim ├── example_7_websocket_chat │ ├── app.nim │ ├── public │ │ └── css │ │ │ └── styles.css │ └── templates │ │ ├── index.html │ │ └── layout.html └── example_8_requestHook_app │ └── app.nim ├── src ├── xander.nim └── xander │ ├── cli.nim │ ├── constants.nim │ ├── contenttype.nim │ ├── install.sh │ ├── tools.nim │ ├── types.nim │ ├── ws.nim │ └── zip │ └── zlib_modified.nim └── xander.nimble /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Santeri J.P. Sydänmetsä 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xander 2 | Xander is an easy to use web application development library and framework for the [Nim programming language](https://nim-lang.org). Nim is a statically typed language with a Python-like syntax and a powerful macro system, something Xander uses greatly to its advantage. 3 | 4 | ## Installation 5 | The easiest way to install Xander is to use [Nimble](https://github.com/nim-lang/nimble), which is bundled with the Nim installation. 6 | 7 | ```nimble install https://github.com/sunjohanday/Xander.git``` 8 | 9 | Otherwise you can download this git repository and ```import xander``` with the appropriate relative file path, e.g. ```import ../xander/xander``` 10 | 11 | **OPTIONAL** If you wish to install Xander CLI, enter the following line on the command line (on Linux): 12 | 13 | ```~/.nimble/pkgs/Xander-0.6.0/Xander/install.sh``` 14 | 15 | You can manually perform the tasks the CLI ```install.sh``` script performs. Simply compile the downloaded ```xander.nim``` file and run the executable. 16 | 17 | A basic Xander-app example: 18 | ```nim 19 | import xander 20 | 21 | get "/": 22 | respond "Hello World!" 23 | 24 | runForever(3000) 25 | ``` 26 | More examples can be found in the ```examples``` folder. 27 | 28 | ## The Gist of It 29 | Xander injects variables for the developer to use in request handlers. These variables are: 30 | 31 | - request, the http request 32 | - data, contains data sent from the client such as get parameters and form data (shorthad for JsonNode) 33 | - headers, for setting response headers (see request.headers for request headers) 34 | - cookies, for accessing request cookies and setting response cookies 35 | - session, client specific session variables 36 | - files, uploaded files 37 | 38 | ```nim 39 | # Request Handler definition 40 | type 41 | RequestHandler* = 42 | proc(request: Request, data: var Data, headers: var HttpHeaders, cookies: var Cookies, session: var Session, files: var UploadFiles): Response {.gcsafe.} 43 | ``` 44 | 45 | These variables do a lot of the legwork required in an effective web application. 46 | 47 | ## Serving files 48 | To serve files from a directory (and its sub-directories) 49 | ```nim 50 | # app.nim 51 | # the app dir contains a directory called 'public' 52 | serveFiles "/public" 53 | ``` 54 | ```html 55 | 56 | 57 | 58 | ``` 59 | 60 | ## Templates 61 | Xander provides support for templates, although it is very much a work in progress. 62 | To serve a template file: 63 | ```nim 64 | # Serve the index page 65 | respond tmplt("index") 66 | ``` 67 | The default directory for templates is ```templates```, but it can also be changed by calling 68 | ```nim 69 | setTemplateDirectory("views") 70 | ``` 71 | By having a ```layout.html``` template one can define a base layout for their pages. 72 | ```html 73 | 74 | 75 | 76 | {[title]} 77 | 78 | 79 | {[ content ]} 80 | {[ template footer ]} 81 | 82 | 83 | ``` 84 | In the example above, ```{[title]}``` is a user defined variable, whereas ```{[ content ]}``` is a Xander defined variable, that contains the contents of a template file. To include your own templates, use the ```template``` keyword ```{[template my-template]}```. You can also include templates that themselves include other templates. 85 | 86 | ```html 87 | 88 | 96 | ``` 97 | You can also seperate templates into directories. The nearest layout file will be used: if none is found in the same directory, parent directories will be searched. 98 | ``` 99 | appDir/ 100 | app.nim 101 | ... 102 | templates/ 103 | 104 | index.html # Root page index 105 | layout.html # Root page layout 106 | 107 | register/ 108 | index.html # Register page index 109 | # Root page layout 110 | 111 | admin/ 112 | index.html # Admin page index 113 | layout.html # Admin page layout 114 | 115 | normie/ 116 | index.html # Client page index 117 | layout.html # Client page layout 118 | 119 | ``` 120 | 121 | ### For loops 122 | For loops are supported in Xander templates. This is still very much a work in progress. 123 | ```html 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {[ for person in people ]} 134 | 135 | 136 | 137 | 144 | 145 | {[ end ]} 146 | 147 |
NameAgeHobbies
{[ person.name ]}{[ person.age ]} 138 |
    139 | {[ for hobby in person.hobbies ]} 140 |
  • {[ hobby ]}
  • 141 | {[ end ]} 142 |
143 |
148 | 149 | 150 | ``` 151 | 152 | ### Template variables 153 | Xander provides a custom type ```Data```, which is shorthand for ```JsonNode```, and it also adds some functions to make life easier. To initialize it, one must use the ```newData()``` func. In the initialized variable, one can add key-value pairs 154 | ```nim 155 | var vars = newData() 156 | vars["name"] = "Alice" 157 | vars["age"] = 21 158 | 159 | vars.set("weight", 50) 160 | 161 | # or you can initialize it with a key-value pair 162 | var vars = newData("name", "Alice").put("age", 21) 163 | ``` 164 | In a template, one must define the variables with matching names. Currently, if no variables are provided, the values will default to empty strings. 165 | ```html 166 |

{[name]} is {[age]} years old.

167 | ``` 168 | 169 | ## Dynamic routes 170 | To match a custom route and get the provided value(s), one must simply use a colon to specify a dynamic value. The values will be stored in the ```data``` parameter implicitly. 171 | ```nim 172 | # User requests /countries/ireland/people/paddy 173 | get "/countries/:country/people/:person": 174 | assert(data["country"] == "ireland") 175 | assert(data["person"] == "paddy") 176 | respond tmplt("userPage", data) 177 | ) 178 | ``` 179 | ```html 180 |

{[person]} is from {[country]}

181 | ``` 182 | 183 | ## Subdomains 184 | To add a subdomain to your application simply do the following: 185 | ```nim 186 | subdomain "api": 187 | 188 | # Matches api.mysite.com 189 | get "/": 190 | respond %* { 191 | "status": "OK", 192 | "message": "Hello World!" 193 | } 194 | 195 | # Matches api.mysite.com/people 196 | get "/people": 197 | let people = @["adam", "beth", "charles", "david", "emma", "fiona"] 198 | respond newData("people", people) 199 | ``` 200 | 201 | ## Hosts 202 | Xander features the ```host``` macro, which makes it possible to run seperate applications depending on the ```hostname``` of the request header. 203 | ```nim 204 | # Travel Blog 205 | host "travel-blog.com": 206 | get "/": 207 | respond "Welcome to my travel blog!" 208 | 209 | # Tech page 210 | host "cool-techy-site.com": 211 | 212 | get "/": 213 | respond "Welcome to my cool techy site!" 214 | 215 | subdomain "api": 216 | get "/": 217 | respond %* { 218 | "status": "OK", 219 | "message": "Welcome" 220 | } 221 | ``` 222 | ## Web Sockets 223 | Xander uses the *ws* library provided by [https://github.com/treeform/ws](https://github.com/treeform/ws). 224 | ```nim 225 | get "/": 226 | respond tmplt("index") 227 | 228 | # echo web socket server 229 | websocket "/ws": 230 | # the websocket variable is injected as 'ws' 231 | while ws.readyState == Open: 232 | let packet = await ws.receiveStrPacket() 233 | await ws.send(packet) 234 | ``` 235 | ## Request Hook 236 | As Xander's request handlers only prepare the response to be sent to the client, a way for accessing the *onRequest* procedure call was added. 237 | 238 | Xander exports a variable called *requestHook*, which the programmer can asign values to. The value should be a anonymous proc as specified below. 239 | ```nim 240 | # app.nim 241 | requestHook = proc(r: Request) {.async.} = 242 | # Do stuff with the request. 243 | # Nothing actually needs to be done. 244 | # The requestHook procedure is run as soon as 245 | # the request is caught by asynchttpserver. 246 | # 247 | # You could basically make your entire app here. 248 | discard 249 | ``` 250 | ```nim 251 | # app.nim 252 | import xander 253 | 254 | # the request hook is essentially the same as 255 | # the 'cb' proc of asynchttpserver.serve, with 256 | # the exception that no responding needs to be done 257 | # (as Xander does it anyways) 258 | requestHook = proc(r: Request) {.async.} = 259 | await r.respond( Http200, "Hello World!" ) 260 | 261 | runForever(3000) 262 | ``` 263 | The *requestHook* can be used with regular Xander request handlers as per usual. 264 | ```nim 265 | import xander 266 | 267 | get "/": 268 | respond tmplt("index") 269 | 270 | requestHook = proc(r: Request) {.async.} = 271 | var ws = await newWebsocket(req) 272 | await ws.sendPacket("Welcome to my echo server!") 273 | while ws.readyState == Open: 274 | let packet = await ws.receiveStrPacket() 275 | await ws.send(packet) 276 | 277 | runForever(3000) 278 | ``` 279 | ## TODO 280 | - Expanding templates with if-statements 281 | - Windows optimization 282 | - HTTPS, this requires a look into **asyncnet** and/or **net** 283 | - Redoing the server architecture without **asynchttpserver** 284 | -------------------------------------------------------------------------------- /examples/example_1_hello_world/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | get "/": 4 | # 5 | # The injected variables 6 | # request: Request 7 | # data: var Data* 8 | # headers: var Headers 9 | # cookies: var Cookies* 10 | # session: var Session* 11 | # files: var UploadFiles* 12 | # exist in this block implicitly. 13 | # (* marks Xander's own data types) 14 | # 15 | # The return type of this method is Xander's own Response type, which 16 | # is a tuple[body: string, httpCode: HttpCode, headers: HttpHeaders]. 17 | # Returning the 'respond' proc call returns this tuple 18 | # 19 | respond "Hello World!" 20 | 21 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_2/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | serveFiles "/public" 4 | setTemplateDirectory "/templates" 5 | 6 | get "api./": 7 | respond "API ROOT" 8 | 9 | get "api./people": 10 | let people = @["adam", "beth", "charles", "david", "emma", "fiona"] 11 | respond newData("people", people) 12 | 13 | get "/": 14 | var uploadedFiles = "" 19 | data["UploadedFiles"] = uploadedFiles 20 | respond tmplt("index", data) 21 | 22 | get "/remove": 23 | var removed = false 24 | if data.hasKey("f"): 25 | let filePath = data.get("f") 26 | if existsFile(filePath): 27 | removeFile(filePath) 28 | removed = true 29 | let status = if not removed: " not" else: "" 30 | redirect("/", "File was" & status & " removed", 6) 31 | 32 | post "/upload": 33 | var filesString = "

File Upload Status

You will be redirected in 10 seconds

" 41 | headers["refresh"] = "10;url=\"/\"" 42 | respond html(filesString) 43 | 44 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_2/public/images/kiira.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santerijps/xander/ed5b5d3ad3443298979e6cb69ef2fbc57d05fb0a/examples/example_2/public/images/kiira.jpg -------------------------------------------------------------------------------- /examples/example_2/public/images/road.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santerijps/xander/ed5b5d3ad3443298979e6cb69ef2fbc57d05fb0a/examples/example_2/public/images/road.jpg -------------------------------------------------------------------------------- /examples/example_2/public/videos/burger.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santerijps/xander/ed5b5d3ad3443298979e6cb69ef2fbc57d05fb0a/examples/example_2/public/videos/burger.webm -------------------------------------------------------------------------------- /examples/example_2/templates/index.html: -------------------------------------------------------------------------------- 1 |

Index

2 | 3 |
4 | 5 |

File Upload

6 |
7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |

Uploaded files on server

15 | {[UploadedFiles]} -------------------------------------------------------------------------------- /examples/example_2/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Xander Example 1 5 | 6 | 7 | 8 | 9 | {[ content ]} 10 | 11 | -------------------------------------------------------------------------------- /examples/example_3_domains/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | include domains/main 4 | include domains/api 5 | 6 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_3_domains/domains/api.nim: -------------------------------------------------------------------------------- 1 | import ../../../src/xander 2 | 3 | # api.mysite.com 4 | subdomain API: 5 | 6 | # api.mysite.com/ 7 | get "/": 8 | respond "API Index" 9 | 10 | # api.mysite.com/countries 11 | router "/countries": 12 | 13 | # api.mysite.com/countries/ 14 | get "/": 15 | respond "Countries Index" 16 | 17 | # api.mysite.com/countries/:search 18 | get "/:search": 19 | let search = data.get("search") 20 | respond "You searched for this country: " & search -------------------------------------------------------------------------------- /examples/example_3_domains/domains/main.nim: -------------------------------------------------------------------------------- 1 | import ../../../src/xander 2 | 3 | get "/": 4 | respond "Index" 5 | 6 | router "/about": 7 | get "/": 8 | redirect "/about/us" 9 | get "/us": 10 | respond "We are a small scale company." 11 | get "/sponsors": 12 | respond "We have multiple sponsors." -------------------------------------------------------------------------------- /examples/example_4_host_subdomain_router/server.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | host "localhost": 4 | 5 | get "/": 6 | respond "Welcome to localhost!" 7 | 8 | router "/about": 9 | 10 | get "/": 11 | redirect "/about/us" 12 | 13 | get "/us": 14 | respond "Us or we?" 15 | 16 | subdomain "api": 17 | 18 | get "/": 19 | respond "You will find many interesting things in this API..." 20 | 21 | router "/countries": 22 | 23 | get "/": 24 | respond "List of countries" 25 | 26 | get "/:search": 27 | let search = data.get("search") 28 | respond "You searched for " & search 29 | 30 | 31 | printServerStructure() 32 | runForever 3000 -------------------------------------------------------------------------------- /examples/example_5_withData/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | get "/": 4 | respond html""" 5 |
6 | 7 | 8 | 9 |
10 | """ 11 | 12 | post "/submit": 13 | 14 | # Macro for: 15 | # 16 | # if data.hasKey("email") and data.hasKey("password"): 17 | # var email = data.get("email") 18 | # var password = data.get("password") 19 | # 20 | withData "email", "password": 21 | return redirect("/", "Your email is " & email & " and your password is " & password, 5) 22 | 23 | respond Http400 24 | 25 | 26 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_6_fetch/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | get "/": 4 | respond "Index" 5 | 6 | get "/users": 7 | let users = fetch("https://jsonplaceholder.typicode.com/users") 8 | respond users 9 | 10 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_7_websocket_chat/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | from strutils import format 3 | from random import randomize, rand 4 | 5 | randomize() 6 | 7 | get "/": 8 | respond tmplt("index") 9 | 10 | type 11 | Client = tuple 12 | id: int 13 | socket: WebSocket 14 | 15 | var clients {.threadvar.} : seq[Client] 16 | clients = newSeq[Client]() 17 | 18 | proc addClient(socket: WebSocket): Client = 19 | result = (rand(1000), socket) 20 | clients.add(result) 21 | 22 | proc sendMessageToAll(message: string) {.async.} = 23 | for client in clients: 24 | if client.socket.readyState == Open: 25 | await client.socket.send(message) 26 | 27 | websocket "/ws": 28 | var client = addClient(ws) 29 | while ws.readyState == Open: 30 | let packet = await ws.receiveStrPacket() 31 | let message = "anon#$1: $2".format(client.id, packet) 32 | await sendMessageToAll(message) 33 | 34 | serveFiles("/public") 35 | printServerStructure() 36 | runForever(3000) -------------------------------------------------------------------------------- /examples/example_7_websocket_chat/public/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | min-height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0 auto; 7 | min-height: 100%; 8 | font-family: Arial; 9 | } -------------------------------------------------------------------------------- /examples/example_7_websocket_chat/templates/index.html: -------------------------------------------------------------------------------- 1 |

Xander Chat

2 | 3 | 12 | 13 |
14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /examples/example_7_websocket_chat/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | example_7 15 | 16 | 17 | 18 | 19 |
20 | {[ content ]} 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/example_8_requestHook_app/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/xander 2 | 3 | requestHook = proc(r: Request) {.async.} = 4 | await r.respond( Http200, "Hello World!") 5 | 6 | runForever(3000) -------------------------------------------------------------------------------- /src/xander.nim: -------------------------------------------------------------------------------- 1 | import 2 | asyncnet, 3 | asyncdispatch as async, 4 | asynchttpserver as http, 5 | cookies as cookies_module, 6 | base64, 7 | httpclient, 8 | json, 9 | logging, 10 | macros, 11 | os, 12 | random, 13 | regex, 14 | std/sha1, 15 | strformat, 16 | strtabs, 17 | strutils, 18 | tables, 19 | times, 20 | typetraits, 21 | uri 22 | 23 | import # Local imports 24 | xander/constants, 25 | xander/contenttype, 26 | xander/tools, 27 | xander/types, 28 | xander/ws, 29 | xander/zip/zlib_modified 30 | 31 | export # TODO: What really needs to be exported??? 32 | async, 33 | cookies_module, 34 | contenttype, 35 | http, 36 | json, 37 | os, 38 | tables, 39 | types, 40 | tools, 41 | ws 42 | 43 | randomize() 44 | 45 | var # Global variables 46 | # Directories 47 | applicationDirectory {.threadvar.} : string 48 | templateDirectory {.threadvar.} : string 49 | # Storage 50 | sessions {.threadvar.} : Sessions 51 | # Fundamental server variables 52 | xanderServer {.threadvar.} : AsyncHttpServer 53 | xanderRoutes {.threadvar.} : Hosts 54 | # Logger 55 | logger {.threadvar.} : Logger 56 | 57 | let appDir = getAppDir() 58 | applicationDirectory = if appDir.extractFilename == "bin": appDir.parentDir() else: appDir 59 | templateDirectory = "templates" 60 | sessions = newSessions() 61 | xanderServer = newAsyncHttpServer(maxBody = 2147483647) # 32 bits MAXED 62 | xanderRoutes = newHosts() 63 | logger = newConsoleLogger(fmtStr="[$time] - XANDER($levelname) ") 64 | 65 | proc setTemplateDirectory*(path: string): void = 66 | var path = path 67 | if path.startsWith("/"): 68 | path = path.substr(1) 69 | templateDirectory = path 70 | 71 | #[ 72 | 73 | NOTE! Known templating bug: 74 | - Regex doesn't always match tags as it should. This means that some variables in the template do not get assigned 75 | any values. To fix this, in the template, add a few tabs or new lines before the problematic variable. 76 | 77 | ]# 78 | 79 | const contentRE = "\\{\\[\\s*content\\s*\\]\\}" 80 | const forRE = "\\{\\[\\s*for\\s+(\\w+)\\s+in\\s+([\\w+\\.?]+)\\s*\\]\\}([\\s\\S]*)\\{\\[\\s*end\\s*\\]\\}" 81 | const templateRE = "\\{\\[\\s*template\\s*(\\S+)\\s*\\]\\}" 82 | const varRE = "\\{\\[([\\s\\S]*?)\\]\\}" 83 | 84 | proc unquote(s: string): string = 85 | s.strip(chars = {'"'}) 86 | 87 | # Checks if the specified 'path' exists in provided JsonNode 'src'. 88 | # If it does, assign its value to 'dest' and return true. 89 | proc dotwalk(src: JsonNode, path: string, dest: var JsonNode): bool = 90 | let keys = path.split('.') 91 | dest = src 92 | result = true 93 | for i in 0 .. keys.high: 94 | let key = keys[i] 95 | if dest.kind == JObject: 96 | if dest.hasKey(key): 97 | dest = dest[key] 98 | else: 99 | return false 100 | elif dest.kind == JArray: 101 | try: 102 | let elems = dest.getElems() 103 | dest = elems[key.parseInt] 104 | except: 105 | return false 106 | 107 | # Gets the appropriate layout file if one exists. 108 | # Prioritizes the layout file in the same directory. 109 | # Then checks parent directories for the file. 110 | proc getLayout(templateName: string): string = 111 | let path = templateName.split( '/' ) 112 | let dirs = path[ 0 .. ^2 ] 113 | var i = dirs.high 114 | while i > -1: 115 | let dir = dirs[ 0 .. i ].join( "/" ) 116 | for file in walkFiles( dir / "*" ): 117 | if file.splitFile.name == "layout": 118 | return readFile(file) 119 | dec(i) 120 | 121 | proc getTemplate(templateName: string, templateContent: var string, imported = false): bool = 122 | var layout = if imported: "" else: getLayout(templateName) 123 | for file in walkFiles(templateName & "*"): 124 | if file.splitFile.name == templateName.splitFile.name: 125 | templateContent = readFile(file) 126 | var match: RegexMatch 127 | if find(layout, re(contentRE), match): 128 | layout[match.boundaries] = templateContent 129 | templateContent = layout 130 | return true 131 | 132 | proc findAndInsertRegularVariables(doc: string, data: JsonNode): string = 133 | result = doc 134 | for match in findAll(result, re(varRe)): 135 | var slice = match.group(0)[0] 136 | let tag = result[slice].strip 137 | slice = slice.a - 2 .. slice.b + 2 138 | var node: JsonNode 139 | if data.dotwalk(tag, node): 140 | result[slice] = ($node).unquote 141 | 142 | proc findAndInsertForLoops(doc: string, data: JsonNode): string = 143 | result = doc 144 | for match in findAll(result, re(forRE)): 145 | # {[ for [item] in [iterable] ]} [body] {[ end ]} 146 | let item = result[match.group(0)[0]] 147 | let iterable = result[match.group(1)[0]] 148 | let body = result[match.group(2)[0]] 149 | # The looped content 150 | var forBody: string 151 | var elements: JsonNode 152 | if data.dotwalk(iterable, elements): 153 | for element in elements.getElems: 154 | let temp = %* { item: element } 155 | forBody &= findAndInsertRegularVariables(body, temp) & '\n' 156 | var m: RegexMatch # Inner for-loop 157 | if find(forBody, re(forRe), m): 158 | forBody = findAndInsertForLoops(forBody, temp) 159 | result[match.boundaries] = forBody 160 | 161 | proc findAndInsertTemplates(doc: string, data: JsonNode): string = 162 | result = doc 163 | for match in findAll(result, re(templateRE)): 164 | let templateName = templateDirectory / result[match.group(0)[0]] 165 | var templateContent: string 166 | if getTemplate(templateName, templateContent, true): 167 | templateContent = findAndInsertTemplates(templateContent, data) 168 | templateContent = findAndInsertRegularVariables(templateContent, data) 169 | templateContent = findAndInsertForLoops(templateContent, data) 170 | result[match.boundaries] = templateContent 171 | else: 172 | echo "Template '$1' not found!".format(templateName) 173 | 174 | proc tmplt*(templateName: string, data: JsonNode = newJObject()): string = 175 | var templateContent: string 176 | if getTemplate(templateDirectory / templateName, templateContent): 177 | result = templateContent 178 | result = findAndInsertTemplates(result, data) 179 | result = findAndInsertRegularVariables(result, data) 180 | result = findAndInsertForLoops(result, data) 181 | else: 182 | echo "Template '$1' not found!".format(templateName) 183 | 184 | proc html*(content: string): string = 185 | # Let's the browser know that the response should be treated as HTML 186 | "\n" & content 187 | 188 | proc respond*(httpCode = Http200, content = "", headers = newHttpHeaders()): types.Response = 189 | return (content, httpCode, headers) 190 | 191 | proc respond*(content: string, httpCode = Http200, headers = newHttpHeaders()): types.Response = 192 | return (content, httpCode, headers) 193 | 194 | proc respond*(data: Data, httpCode = Http200, headers = newHttpHeaders()): types.Response = 195 | return ($data, httpCode, headers) 196 | 197 | proc respond*(file: UploadFile, httpCode = Http200, headers = newHttpHeaders()): types.Response = 198 | headers["Content-Type"] = getContentType(file.ext) 199 | return (file.content, httpCode, headers) 200 | 201 | proc redirect*(path: string, content = "", delay = 0, httpCode = Http303): types.Response = 202 | ( content, httpCode, 203 | if delay == 0: 204 | newHttpHeaders([("location", path)]) 205 | else: 206 | newHttpHeaders([("refresh", &"{delay};url=\"{path}\"")]) 207 | ) 208 | 209 | proc serve(request: Request, httpCode: HttpCode, content = "", headers = newHttpHeaders()): Future[void] {.async.} = 210 | await request.respond(httpCode, content, headers) 211 | 212 | proc serveError(request: Request, httpCode: HttpCode = Http500, message = ""): Future[void] {.gcsafe, async.} = 213 | var content = message 214 | if existsFile(templateDirectory / "error.html"): 215 | var data = newData() 216 | data["title"] = "Internal Server Error" 217 | data["code"] = $httpCode 218 | data["message"] = message 219 | content = tmplt("error", data) 220 | else: 221 | content = &"

({httpCode}) Error


{message}

" 222 | content = html(content) 223 | await serve(request, httpCode, content) 224 | 225 | proc parseFormMultiPart(body, boundary: string, data: var Data, files: var UploadFiles): void = 226 | let fileInfos = body.split(boundary) # Contains: Content-Disposition, Content-Type, Others... and actual content 227 | var 228 | parts: seq[string] 229 | fileName, fileExtension, content, varName: string 230 | size: int 231 | for fileInfo in fileInfos: 232 | if "Content-Disposition" in fileInfo: 233 | parts = fileInfo.split("\c\n\c\n", 1) 234 | assert parts.len == 2 235 | for keyVals in parts[0].split(";"): 236 | if " name=" in keyVals: 237 | varName = keyVals.split(" name=\"")[1].strip(chars = {'"'}) 238 | if " filename=" in keyVals: 239 | fileName = keyVals.split(" filename=\"")[1].split("\"")[0] 240 | fileExtension = if '.' in fileName: fileName.split(".")[1] else: "file" 241 | content = parts[1][0..parts[1].len - 3] # Strip the last two characters out = \r\n 242 | size = content.len 243 | # Add variables to Data and files to UploadFiles 244 | if fileName.len == 0: # Data 245 | data[varName] = content#.strip() 246 | else: # UploadFiles 247 | if not files.hasKey(varName): 248 | files[varName] = newSeq[UploadFile]() 249 | files[varName].add(newUploadFile(fileName, fileExtension, content, size)) 250 | varName = ""; fileName = ""; content = "" 251 | 252 | proc uploadFile*(directory: string, file: UploadFile, name = ""): void = 253 | let fileName = if name == "": file.name else: name 254 | var filePath = if directory.endsWith("/"): directory & fileName else: directory & "/" & fileName 255 | try: 256 | writeFile(filePath, file.content) 257 | except IOError: 258 | logger.log(lvlError, "IOError: Failed to write file") 259 | 260 | proc uploadFiles*(directory: string, files: seq[UploadFile]): void = 261 | var filePath: string 262 | let dir = if directory.endsWith("/"): directory else: directory & "/" 263 | for file in files: 264 | filePath = dir & file.name 265 | try: 266 | writeFile(filePath, file.content) 267 | except IOError: 268 | logger.log(lvlError, &"IOError: Failed to write file '{filePath}'") 269 | 270 | proc parseForm(body: string): JsonNode = 271 | result = newJObject() 272 | # Use decodeUrl(body, false) if plusses (+) should not 273 | # be considered spaces. 274 | let urlDecoded = decodeUrl(body) 275 | let kvPairs = urlDecoded.split("&") 276 | for kvPair in kvPairs: 277 | # Assuming the key doesn't contain equals signs, 278 | # but the value does, split once 279 | let kvArray = kvPair.split("=", 1) 280 | var key = kvArray[0] 281 | let value = kvArray[1] 282 | if "[]" in key: 283 | key = key[0 .. key.len - 3] 284 | if result.hasKey(key): 285 | var arr = result[key].getElems() 286 | arr.add(newJString(value)) 287 | result.set(key, arr) 288 | else: 289 | let arr = newJArray() 290 | arr.add(newJString(value)) 291 | result.set(key, arr) 292 | else: 293 | result.set(key, value) 294 | 295 | proc getJsonData(keys: OrderedTable[string, JsonNode], node: JsonNode): JsonNode = 296 | result = newJObject() 297 | for key in keys.keys: 298 | result{key} = node[key] 299 | 300 | proc parseRequestBody(body: string): JsonNode = 301 | try: # JSON 302 | var 303 | parsed = json.parseJson(body) 304 | keys = parsed.getFields() 305 | return getJsonData(keys, parsed) 306 | except: # Form 307 | return parseForm(body) 308 | 309 | func parseUrlQuery(query: string, data: var Data): void = 310 | let query = decodeUrl(query) 311 | if query.len > 0: 312 | for parameter in query.split("&"): 313 | if "=" in parameter: 314 | let pair = parameter.split("=") 315 | data[pair[0]] = pair[1] 316 | else: 317 | data[parameter] = true 318 | 319 | proc isValidGetPath(url, kind: string, params: var Data): bool = 320 | # Checks if the given 'url' matches 'kind'. As the 'url' can be dynamic, 321 | # the checker will ignore differences if a 'kind' subpath starts with a colon. 322 | var 323 | kind = kind.split("/") 324 | klen = kind.len 325 | url = url.split("/") 326 | result = true 327 | if url.len == klen: 328 | for i in 0..klen-1: 329 | if ":" in kind[i]: 330 | params[kind[i][1 .. kind[i].len - 1]] = url[i] 331 | elif kind[i] != url[i]: 332 | result = false 333 | break 334 | else: 335 | result = false 336 | 337 | proc hasSubdomain(host: string, subdomain: var string): bool = 338 | let domains = host.split('.') 339 | let count = domains.len 340 | if count > 1: # ["api", "mysite", "com"] ["mysite", "com"] ["api", "localhost"] ["192", "168", "1", "43"] 341 | if count == 3 and domains[0] == "www": 342 | return false 343 | elif count == 4: 344 | var isIpAddress = true 345 | for d in domains: 346 | try: 347 | discard parseInt(d) 348 | except: 349 | isIpAddress = false 350 | break 351 | if isIpAddress: 352 | subdomain = defaultDomain 353 | return true 354 | elif (count >= 3) or (count == 2 and domains[1].split(":")[0] == "localhost"): 355 | subdomain = domains[0] 356 | result = true 357 | 358 | proc checkPath(request: Request, kind: string, data: var Data, files: var UploadFiles): bool = 359 | # For get requests, checks that the path (which could be dynamic) is valid, 360 | # and gets the url parameters. For other requests, the request body is parsed. 361 | # The 'kind' parameter is an existing route. 362 | if request.reqMethod == HttpGet: # URL parameters 363 | result = isValidGetPath(request.url.path, kind, data) 364 | else: # Form body 365 | let contentType = request.headers["Content-Type"].split(";") # TODO: Use me wisely to detemine how to parse request body 366 | if request.url.path == kind: 367 | result = true 368 | if request.body.len > 0: 369 | if "multipart/form-data" in contentType: # File upload 370 | let boundary = "--" & contentType[1].split("=")[1] 371 | parseFormMultiPart(request.body, boundary, data, files) 372 | else: # Other 373 | data = parseRequestBody(request.body) 374 | 375 | proc setResponseCookies*(response: var types.Response, cookies: Cookies): void = 376 | for key, cookie in cookies.server: 377 | response.headers.add("Set-Cookie", 378 | cookies_module.setCookie( 379 | cookie.name, cookie.value, cookie.domain, cookie.path, cookie.expires, true, cookie.secure, cookie.httpOnly)) 380 | 381 | # TODO: This is not very random 382 | # SHA-1 Hash 383 | proc generateSessionId(): string = 384 | $secureHash($(cpuTime() + rand(10000).float)) 385 | 386 | proc setResponseHeaders(response: var types.Response, headers: HttpHeaders): void = 387 | for key, val in headers.pairs: 388 | response.headers.add(key, val) 389 | 390 | proc getSession(cookies: var Cookies, session: var Session): string = 391 | # Gets a session if one exists. Initializes a new one if it doesn't. 392 | var ssid: string 393 | if not cookies.contains("XANDER-SSID"): 394 | ssid = $generateSessionId() 395 | sessions[ssid] = session 396 | cookies.set("XANDER-SSID", ssid, httpOnly = true) 397 | else: 398 | ssid = cookies.get("XANDER-SSID") 399 | if sessions.hasKey(ssid): 400 | session = sessions[ssid] 401 | return ssid 402 | 403 | # The default content-type header is text/plain. 404 | proc setContentTypeToHTMLIfNeeded(response: types.Response, headers: var HttpHeaders): void = 405 | if "" in response.body: 406 | headers["Content-Type"] = "text/html; charset=UTF-8" 407 | 408 | proc setDefaultHeaders(headers: var HttpHeaders): void = 409 | headers["Cache-Control"] = "public; max-age=" & $(60*60*24*7) # One week 410 | headers["Connection"] = "keep-alive" 411 | headers["Content-Type"] = "text/plain; charset=UTF-8" 412 | headers["Content-Security-Policy"] = "font-src 'self'" 413 | headers["Feature-Policy"] = "autoplay 'none'" 414 | headers["Referrer-Policy"] = "no-referrer" 415 | headers["Server"] = "xander" 416 | headers["Vary"] = "User-Agent, Accept-Encoding" 417 | headers["X-Content-Type-Options"] = "nosniff" 418 | headers["X-Frame-Options"] = "DENY" 419 | headers["X-XSS-Protection"] = "1; mode=block" 420 | 421 | # TODO: Some say .pngs should not be compressed 422 | proc gzip(response: var types.Response, request: Request, headers: var HttpHeaders): void = 423 | if request.headers.hasKey("accept-encoding"): 424 | if "gzip" in request.headers["accept-encoding"]: 425 | try: 426 | # Uses a modified version of zip/zlib.nim 427 | response.body = compress(response.body, response.body.len, Z_DEFLATED) 428 | headers["content-encoding"] = "gzip" 429 | except: 430 | # Deprecated, since the modified version is used 431 | logger.log(lvlError, "Failed to gzip compress. Did you set 'Type Ulong* = uint'?") 432 | 433 | proc parseHostAndDomain(request: Request): tuple[host, domain: string] = 434 | result = (defaultHost, defaultDomain) 435 | if request.headers.hasKey("host"): 436 | let url = $request.headers["host"].split(':')[0] # leave the port out of this! 437 | let parts = url.split('.') 438 | result = case parts.len: 439 | of 1: 440 | (parts[0], defaultDomain) # localhost 441 | of 2: 442 | if parts[1] == "localhost": 443 | (parts[1], parts[0]) # api.localhost 444 | else: 445 | (parts[0], defaultDomain) # site.com 446 | of 3: 447 | (parts[1], parts[0]) # api.site.com 448 | else: 449 | # TODO: NOT GOOD AT ALL 450 | (defaultHost, defaultDomain) 451 | 452 | proc getHostAndDomain(request: Request): tuple[host, domain: string] = 453 | if xanderRoutes.isDefaultHost(): 454 | var subdomain: string 455 | if request.headers.hasKey("host") and hasSubdomain(request.headers["host"], subdomain): 456 | (defaultHost, subdomain) 457 | else: 458 | (defaultHost, defaultDomain) 459 | else: 460 | parseHostAndDomain(request) 461 | 462 | # EXPERIMENTAL 463 | type RequestHookProc* = proc(r: Request): Future[void] {.gcsafe.} 464 | var requestHook* {.threadvar.} : RequestHookProc # == nil 465 | 466 | # EXPERIMENTAL 467 | type WebSocketHandler* = proc(ws: WebSocket): Future[void] {.gcsafe.} 468 | var websockets {.threadvar.} : Table[string, WebSocketHandler] 469 | websockets = initTable[string, WebSocketHandler]() 470 | 471 | proc handleWebSockets(request: Request): Future[void] {.async,gcsafe.} = 472 | if websockets.hasKey(request.url.path): 473 | try: 474 | var ws = await newWebSocket(request) 475 | await websockets[request.url.path](ws) 476 | except: 477 | discard 478 | 479 | # TODO: Check that request size <= server max allowed size 480 | # Called on each request to server 481 | proc onRequest(request: Request): Future[void] {.async,gcsafe.} = 482 | # Experimental: Request Hook 483 | if requestHook != nil: 484 | await requestHook(request) 485 | # Experimental: Built-in Web Sockets 486 | await handleWebSockets(request) 487 | # Initialize variables 488 | var (data, headers, cookies, session, files) = newRequestHandlerVariables() 489 | var (host, domain) = getHostAndDomain(request) 490 | if xanderRoutes.existsMethod(request.reqMethod, host, domain): 491 | # At this point, we're inside the domain! 492 | for serverRoute in xanderRoutes[host][domain][request.reqMethod]: 493 | if checkPath(request, serverRoute.route, data, files): 494 | # Get URL query parameters 495 | parseUrlQuery(request.url.query, data) 496 | # Parse cookies from header 497 | let parsedCookies = parseCookies(request.headers.getOrDefault("Cookie")) 498 | # Cookies sent from client 499 | for key, val in parsedCookies.pairs: 500 | cookies.setClient(key, val) 501 | # Create or get session and session ID 502 | let ssid = getSession(cookies, session) 503 | # Set default headers 504 | setDefaultHeaders(headers) 505 | # Request handler response 506 | var response: types.Response 507 | try: 508 | response = serverRoute.handler(request, data, headers, cookies, session, files) 509 | except: 510 | logger.log(lvlError, "Request Handler broke with request path '$1'.".format(request.url.path)) 511 | response.httpCode = Http500 512 | response.body = "Internal server error" 513 | response.headers = newHttpHeaders() 514 | # TODO: Fix the way content type is determined 515 | setContentTypeToHTMLIfNeeded(response, headers) 516 | # gzip encode if needed 517 | gzip(response, request, headers) 518 | # Update session 519 | sessions[ssid] = session 520 | # Cookies set on server => add them to headers 521 | setResponseCookies(response, cookies) 522 | # Put headers into response 523 | setResponseHeaders(response, headers) 524 | await request.respond(response.httpCode, response.body, response.headers) 525 | await serveError(request, Http404) 526 | 527 | # TODO: Check port range 528 | proc runForever*(port: uint = 3000, message: string = "Xander server is up and running!"): void = 529 | logger.log(lvlInfo, message) 530 | defer: close(xanderServer) 531 | waitFor xanderServer.serve(Port(port), onRequest) 532 | 533 | proc addRoute*(host = defaultHost, domain = defaultDomain, httpMethod: HttpMethod, route: string, handler: RequestHandler): void = 534 | xanderRoutes.addRoute(httpMethod, route, handler, host, domain) 535 | 536 | proc addGet*(host, domain, route: string, handler: RequestHandler): void = 537 | addRoute(host, domain, HttpGet, route, handler) 538 | 539 | proc addPost*(host, domain, route: string, handler: RequestHandler): void = 540 | addRoute(host, domain, HttpPost, route, handler) 541 | 542 | proc addPut*(host, domain, route: string, handler: RequestHandler): void = 543 | addRoute(host, domain, HttpPut, route, handler) 544 | 545 | proc addDelete*(host, domain, route: string, handler: RequestHandler): void = 546 | addRoute(host, domain, HttpDelete, route, handler) 547 | 548 | # TODO: Build source using Nim Nodes instead of strings 549 | proc buildRequestHandlerSource(host, domain, reqMethod, route, body: string): string = 550 | var source = &"add{reqMethod}({tools.quote(host)}, {tools.quote(domain)}, {tools.quote(route)}, {requestHandlerString}{newLine}" 551 | for row in body.split(newLine): 552 | if row.len > 0: 553 | source &= &"{tab}{row}{newLine}" 554 | return source & ")" 555 | 556 | macro get*(route: string, body: untyped): void = 557 | let requestHandlerSource = buildRequestHandlerSource(defaultHost, defaultDomain, "Get", unquote(route), repr(body)) 558 | parseStmt(requestHandlerSource) 559 | 560 | macro x_get*(host, domain, route: string, body: untyped): void = 561 | let requestHandlerSource = buildRequestHandlerSource(unquote(host), unquote(domain), "Get", unquote(route), repr(body)) 562 | parseStmt(requestHandlerSource) 563 | 564 | macro post*(route: string, body: untyped): void = 565 | let requestHandlerSource = buildRequestHandlerSource(defaultHost, defaultDomain, "Post", unquote(route), repr(body)) 566 | parseStmt(requestHandlerSource) 567 | 568 | macro x_post*(host, domain, route: string, body: untyped): void = 569 | let requestHandlerSource = buildRequestHandlerSource(unquote(host), unquote(domain), "Post", unquote(route), repr(body)) 570 | parseStmt(requestHandlerSource) 571 | 572 | macro put*(route: string, body: untyped): void = 573 | let requestHandlerSource = buildRequestHandlerSource(defaultHost, defaultDomain, "Put", unquote(route), repr(body)) 574 | parseStmt(requestHandlerSource) 575 | 576 | macro x_put*(host, domain, route: string, body: untyped): void = 577 | let requestHandlerSource = buildRequestHandlerSource(unquote(host), unquote(domain), "Put", unquote(route), repr(body)) 578 | parseStmt(requestHandlerSource) 579 | 580 | macro delete*(route: string, body: untyped): void = 581 | let requestHandlerSource = buildRequestHandlerSource(defaultHost, defaultDomain, "Delete", unquote(route), repr(body)) 582 | parseStmt(requestHandlerSource) 583 | 584 | macro delete*(host, domain, route: string, body: untyped): void = 585 | let requestHandlerSource = buildRequestHandlerSource(unquote(host), unquote(domain), "Delete", unquote(route), repr(body)) 586 | parseStmt(requestHandlerSource) 587 | 588 | proc startsWithRequestMethod(s: string): bool = 589 | let methods = @["get", "post", "put", "delete"] 590 | for m in methods: 591 | if s.startsWith(m): 592 | return true 593 | 594 | proc reformatRouterCode(host, domain, path, body: string): string = 595 | for line in body.split(newLine): 596 | var lineToAdd = line & newLine 597 | if line.len > 0 and ':' in line: 598 | # request handler 599 | if startsWithRequestMethod(line): 600 | let requestHandlerDefinition = line.split(" ") 601 | let requestMethod = requestHandlerDefinition[0] 602 | var route = requestHandlerDefinition[1] 603 | if route == "\"/\":": 604 | route = "\"\"" 605 | route = route[0] & path & route[1..route.len - 1] 606 | lineToAdd = "x_" & requestMethod & " " & tools.quote(host) & ", " & tools.quote(domain) & ", " & route & ":" & newLine 607 | elif route.startsWith('"') and route.endsWith("\":") and route[1] == '/': 608 | route = route[0] & path & route[1..route.len - 1] 609 | lineToAdd = "x_" & requestMethod & " " & tools.quote(host) & ", " & tools.quote(domain) & ", " & route & newLine 610 | result &= lineToAdd 611 | 612 | macro router*(route, body: untyped): void = 613 | let path = repr(route).unquote 614 | let body = reformatRouterCode(defaultHost, defaultDomain, path, repr(body)) 615 | parseStmt(body) 616 | 617 | macro x_router*(host, domain, route, body: untyped): void = 618 | let body = reformatRouterCode(host.unquote, domain.unquote, route.unquote, repr(body)) 619 | parseStmt(body) 620 | 621 | proc reformatSubdomainCode(domain, host, body: string): string = 622 | # transforms request macros and routers to x_gets and x_routers 623 | for line in body.split(newLine): 624 | var lineToAdd = line & newLine 625 | if line.len > 0 and ':' in line: 626 | # request handler not within a host-block 627 | if startsWithRequestMethod(line): 628 | let requestHandlerDefinition = line.split(" ") 629 | let requestMethod = requestHandlerDefinition[0] 630 | let route = requestHandlerDefinition[1] 631 | lineToAdd = "x_" & requestMethod & " " & tools.quote(host) & ", " & tools.quote(domain) & ", " & route & newLine 632 | # router not within a host-block 633 | elif line.startsWith("router"): 634 | let routerDefinition = line.split(" ") 635 | var route = routerDefinition[1] 636 | lineToAdd = "x_router " & tools.quote(host) & ", " & tools.quote(domain) & ", " & route & newLine 637 | result &= lineToAdd 638 | 639 | macro subdomain*(name, body: untyped): void = 640 | let name = repr(name).unquote.toLower 641 | let body = reformatSubdomainCode(name, defaultHost, repr(body)) 642 | parseStmt(body) 643 | 644 | proc reformatHostCode(host, body: string): string = 645 | var domain = defaultDomain 646 | var router = "" 647 | for line in body.split(newLine): 648 | var lineToAdd = line & newLine 649 | if line.len > 0 and ':' in line: 650 | # Subdomain 651 | if strip(line).startsWith("subdomain"): 652 | router = "" 653 | domain = unquote(line.split(" ")[1].replace(":", "")) 654 | # Router 655 | elif strip(line).startsWith("router"): 656 | if indentation(line) == 0: domain = defaultDomain 657 | router = strip(line).split(" ")[1].replace(":", "").unquote 658 | # Request Handler 659 | elif strip(line).startsWithRequestMethod: 660 | let indent = indentation(line) # level of indentation 661 | case indent: 662 | of 0: # just inside host 663 | domain = defaultDomain 664 | router = "" 665 | else: # other options 666 | discard 667 | var line = line[indent .. line.len - 1] 668 | let handlerDef = line.split(" ") 669 | var route = handlerDef[1].unquote.strip(chars = {':'}) 670 | route = if route.len == 1 and router != "": router else: router & route[0 .. route.len - 1] 671 | lineToAdd = repeat(" ", indent) & "x_" & handlerDef[0] & " " & tools.quote(host) & ", " & tools.quote(domain) & ", " & tools.quote(route) & ":" & newLine 672 | # Append line to result 673 | result &= lineToAdd 674 | 675 | macro host*(host, body: untyped): void = 676 | let host = repr(host).unquote.toLower 677 | let body = reformatHostCode(host, repr(body)) 678 | parseStmt(body) 679 | 680 | proc addWebSocketProc*(path: string, p: WebSocketHandler): void = 681 | websockets[path] = p 682 | 683 | macro websocket*(path, body: untyped): void = 684 | var src = "addWebSocketProc($1, proc(ws: WebSocket) {.async.} =\n".format(repr(path)) 685 | for line in repr(body).split( newLine ): 686 | src &= tab & line & newLine 687 | src &= ")" 688 | parseStmt(src) 689 | 690 | proc printServerStructure*(): void = 691 | logger.log(lvlInfo, xanderRoutes) 692 | 693 | # TODO: Dynamically created directories are not supported 694 | proc serveFiles*(route: string): void = 695 | # Given a route, e.g. '/public', this proc 696 | # adds a get method for provided directory and 697 | # its child directories. This proc is RECURSIVE. 698 | var route = route.replace(applicationDirectory, "") 699 | logger.log(lvlInfo, "Serving files from ", applicationDirectory & route) 700 | let path = if route.endsWith("/"): route[0..route.len-2] else: route # /public/ => /public 701 | let newRoute = path & "/:fileName" # /public/:fileName 702 | for host in xanderRoutes.keys: # add file serving for every host 703 | addGet(host, defaultDomain, newRoute, proc(request: Request, data: var Data, headers: var HttpHeaders, cookies: var Cookies, session: var Session, files: var UploadFiles): types.Response {.gcsafe.} = 704 | let filePath = applicationDirectory & path / decodeUrl(data.get("fileName")) # ./public/.../fileName 705 | let ext = splitFile(filePath).ext 706 | if existsFile(filePath): 707 | headers["Content-Type"] = getContentType(ext) 708 | respond readFile(filePath) 709 | else: respond Http404) 710 | for directory in walkDirs(applicationDirectory & path & "/*"): 711 | serveFiles(directory) 712 | 713 | proc fetch*(url: string): string = 714 | var client = newAsyncHttpClient() 715 | waitFor client.getContent(url) 716 | 717 | setControlCHook(proc() {.noconv.} = 718 | close(xanderServer) 719 | quit(0) 720 | ) 721 | 722 | when isMainModule: 723 | 724 | include xander/cli 725 | -------------------------------------------------------------------------------- /src/xander/cli.nim: -------------------------------------------------------------------------------- 1 | import 2 | os, 3 | osproc, 4 | sequtils, 5 | strutils 6 | 7 | proc newProject(appDir = "my-xander-app"): void = 8 | stdout.write "Creating project '$1'... ".format(appDir) 9 | createDir(appDir) 10 | createDir(appDir / "templates") 11 | createDir(appDir / "public") 12 | createDir(appDir / "public" / "css") 13 | createDir(appDir / "public" / "js") 14 | writeFile(appDir / "app.nim", """import xander 15 | 16 | get "/": 17 | respond tmplt("index") 18 | 19 | get "/about": 20 | respond tmplt("about") 21 | 22 | get "/contact": 23 | respond tmplt("contact") 24 | 25 | serveFiles("/public") 26 | printServerStructure() 27 | runForever(3000)""") 28 | writeFile(appDir / "templates" / "layout.html", """ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | $1 42 | 43 | 44 | 45 | 46 |
47 |

$1

48 |

Powered by Xander

49 |
50 | 51 | 64 | 65 |
66 | {[ content ]} 67 |
68 | 69 | 70 | 71 | """.format(appDir)) 72 | writeFile(appDir / "templates" / "index.html", """

Home

73 |
74 |

75 | Congratulations! This app is running Xander! For Xander reference, check out 76 | xander-nim.tk and 77 | Xander's GitHub page. 78 |

""") 79 | writeFile(appDir / "templates" / "about.html", """

About

80 |
81 |

82 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tempor odio quis sem tincidunt, id tincidunt velit rutrum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed lectus nisl, lacinia sed volutpat a, convallis a velit. Etiam rhoncus tincidunt enim, sed interdum justo tincidunt eu. Nulla bibendum dui at mi vulputate, sed eleifend turpis sodales. Vivamus dignissim dapibus blandit. In laoreet mattis dapibus. Pellentesque magna elit, porta sed tristique et, tempus sit amet justo. Curabitur aliquet feugiat tincidunt. Fusce tempor efficitur mi blandit consequat. Praesent sed maximus risus. Nullam nulla purus, pellentesque quis leo sed, tincidunt ultrices enim. Curabitur at ante et justo porta bibendum. Morbi quis bibendum ipsum, a vulputate mi. Nulla blandit sodales diam a pharetra. Phasellus tristique, magna nec dapibus aliquam, felis nisl viverra ipsum, vitae hendrerit felis nisi ut tortor. 83 | Vestibulum ut lobortis dui, sit amet dictum dolor. Integer vitae varius lectus, quis tincidunt mauris. Phasellus sodales ligula non vestibulum faucibus. Duis ac risus eleifend nisl accumsan feugiat. Maecenas vestibulum dolor id nibh molestie, a mollis dolor viverra. Proin at feugiat est. Cras porta suscipit dignissim. Pellentesque maximus libero at eros fringilla tincidunt. Vestibulum ultricies pulvinar finibus. Phasellus id rutrum dui, vel condimentum nunc. Vivamus condimentum aliquet magna ut convallis. Maecenas congue dignissim urna, ac feugiat lorem cursus at. Integer placerat, quam id vulputate sollicitudin, dui neque pellentesque est, sed malesuada mauris ligula non ex. Sed ac dignissim massa, sit amet congue diam. 84 | Ut pellentesque rhoncus ultricies. Aenean non pretium diam. Nunc fringilla ante eleifend, maximus lacus non, euismod orci. Sed non orci ornare, condimentum sapien commodo, efficitur eros. Mauris quis magna sed sapien aliquet feugiat. Duis scelerisque purus libero, et condimentum lacus faucibus eu. Proin porttitor molestie arcu a consectetur. Nunc sed tortor non ipsum finibus condimentum. Duis quis scelerisque sem, vel sagittis lorem. Proin egestas lorem nec augue finibus dapibus. 85 | Donec malesuada ante eu diam tincidunt, id commodo nisi accumsan. Phasellus non dictum justo. Suspendisse pharetra quis dolor eu pretium. Proin consectetur imperdiet lacus quis sollicitudin. Vivamus hendrerit metus bibendum erat semper, id placerat nibh tristique. Suspendisse quis tristique leo, ac consectetur velit. Vivamus dui velit, porta a urna sit amet, tristique vestibulum orci. 86 | Curabitur velit lacus, mattis at quam vitae, ultricies ornare dui. Nam sed elementum orci. Pellentesque eu quam nunc. Morbi pharetra consectetur lorem quis molestie. Praesent viverra arcu elit, venenatis elementum diam commodo a. Integer venenatis turpis vel felis sodales, vitae iaculis odio dignissim. Aliquam erat volutpat. 87 |

""") 88 | writeFile(appDir / "templates" / "contact.html", """

Contact

89 |
90 |

91 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tempor odio quis sem tincidunt, id tincidunt velit rutrum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed lectus nisl, lacinia sed volutpat a, convallis a velit. Etiam rhoncus tincidunt enim, sed interdum justo tincidunt eu. Nulla bibendum dui at mi vulputate, sed eleifend turpis sodales. Vivamus dignissim dapibus blandit. In laoreet mattis dapibus. Pellentesque magna elit, porta sed tristique et, tempus sit amet justo. Curabitur aliquet feugiat tincidunt. Fusce tempor efficitur mi blandit consequat. Praesent sed maximus risus. Nullam nulla purus, pellentesque quis leo sed, tincidunt ultrices enim. Curabitur at ante et justo porta bibendum. Morbi quis bibendum ipsum, a vulputate mi. Nulla blandit sodales diam a pharetra. Phasellus tristique, magna nec dapibus aliquam, felis nisl viverra ipsum, vitae hendrerit felis nisi ut tortor. 92 | Vestibulum ut lobortis dui, sit amet dictum dolor. Integer vitae varius lectus, quis tincidunt mauris. Phasellus sodales ligula non vestibulum faucibus. Duis ac risus eleifend nisl accumsan feugiat. Maecenas vestibulum dolor id nibh molestie, a mollis dolor viverra. Proin at feugiat est. Cras porta suscipit dignissim. Pellentesque maximus libero at eros fringilla tincidunt. Vestibulum ultricies pulvinar finibus. Phasellus id rutrum dui, vel condimentum nunc. Vivamus condimentum aliquet magna ut convallis. Maecenas congue dignissim urna, ac feugiat lorem cursus at. Integer placerat, quam id vulputate sollicitudin, dui neque pellentesque est, sed malesuada mauris ligula non ex. Sed ac dignissim massa, sit amet congue diam. 93 | Ut pellentesque rhoncus ultricies. Aenean non pretium diam. Nunc fringilla ante eleifend, maximus lacus non, euismod orci. Sed non orci ornare, condimentum sapien commodo, efficitur eros. Mauris quis magna sed sapien aliquet feugiat. Duis scelerisque purus libero, et condimentum lacus faucibus eu. Proin porttitor molestie arcu a consectetur. Nunc sed tortor non ipsum finibus condimentum. Duis quis scelerisque sem, vel sagittis lorem. Proin egestas lorem nec augue finibus dapibus. 94 | Donec malesuada ante eu diam tincidunt, id commodo nisi accumsan. Phasellus non dictum justo. Suspendisse pharetra quis dolor eu pretium. Proin consectetur imperdiet lacus quis sollicitudin. Vivamus hendrerit metus bibendum erat semper, id placerat nibh tristique. Suspendisse quis tristique leo, ac consectetur velit. Vivamus dui velit, porta a urna sit amet, tristique vestibulum orci. 95 | Curabitur velit lacus, mattis at quam vitae, ultricies ornare dui. Nam sed elementum orci. Pellentesque eu quam nunc. Morbi pharetra consectetur lorem quis molestie. Praesent viverra arcu elit, venenatis elementum diam commodo a. Integer venenatis turpis vel felis sodales, vitae iaculis odio dignissim. Aliquam erat volutpat. 96 |

""") 97 | writeFile(appDir / "public" / "css" / "styles.css", """html { 98 | min-height: 100%; 99 | } 100 | 101 | body { 102 | margin: 0 auto; 103 | min-height: 100%; 104 | font-family: Arial; 105 | }""") 106 | echo "Done!" 107 | 108 | type 109 | CmdResponse = tuple 110 | output: TaintedString 111 | exitCode: int 112 | 113 | proc cmd(s: string): CmdResponse = 114 | execCmdEx(s) 115 | 116 | proc nimCompile(fileName: string): CmdResponse = 117 | cmd("nim c --threads:on --verbosity:1 --hints:off $1".format(fileName)) 118 | 119 | proc runProject(fileName = "app.nim"): void = 120 | if existsFile(fileName): 121 | discard execCmd("rm ." / fileName.splitFile.name) 122 | discard nimCompile(fileName) 123 | discard execCmd("." / fileName.splitFile.name) 124 | else: 125 | echo "File not found: ", fileName 126 | 127 | proc getProcessId(port: string, pid: var string): bool = 128 | var output = execProcess("lsof -i | grep *:$1".format(port)).strip() 129 | var parts = output.split(" ").filter(proc(x: string): bool = x.len > 0) 130 | if parts.len > 1: 131 | pid = parts[1] 132 | return true 133 | 134 | proc killProcess(pid: string): void = 135 | discard execProcess("kill $1".format(pid)) 136 | 137 | proc updateXander(): void = 138 | echo execCmd("nimble install https://github.com/sunjohanday/xander") 139 | 140 | setControlCHook(proc() {.noconv.} = 141 | quit(0) 142 | ) 143 | 144 | let params = commandLineParams() 145 | 146 | if params.len == 0: 147 | echo """No command provided. Commands are 'new', 'run' and 'listen'.""" 148 | 149 | else: 150 | case params[0]: 151 | 152 | of "new": 153 | if params.len > 1: 154 | newProject(params[1]) 155 | else: 156 | newProject() 157 | 158 | of "run": 159 | if params.len > 1: 160 | runProject(params[1]) 161 | else: 162 | runProject() 163 | 164 | of "listen": 165 | 166 | var fileName {.threadvar.} : string 167 | var execName {.threadvar.} : string 168 | 169 | if params.len > 1: 170 | fileName = params[1] 171 | execName = fileName.splitFile.name 172 | 173 | else: 174 | fileName = "app.nim" 175 | execName = "app" 176 | 177 | var app: Thread[string] 178 | var appProc = proc(execName: string) {.thread, nimcall.} = 179 | let output = execProcess("./$1 &".format(execName)) 180 | if output.len > 0: 181 | echo "\n~~~OUTPUT~~~\n$1\n~~~/OUTPUT~~~\n".format(output) 182 | 183 | var (output, exitCode) = nimCompile(fileName) 184 | 185 | if exitCode == 1: 186 | echo output 187 | else: 188 | createThread(app, appProc, execName) 189 | 190 | proc getPort(): string = 191 | for line in fileName.lines: 192 | if "runForever" in line: 193 | result = line.replace("(", "").replace(")", "").replace(" ", "").replace("runForever", "") 194 | # Linux: lsof command recognizes port 8080 as http-alt 195 | result = if result == "8080": "http-alt" else: result 196 | 197 | var pid: string # Process ID 198 | var previous = readFile(fileName) # File content previously 199 | var next: TaintedString # File content now 200 | var port = getPort() # Application port 201 | 202 | echo "Listening $1 on port $2".format(fileName, port) 203 | 204 | # Infinite loop? 205 | while true: 206 | 207 | # Get file content 208 | next = readFile(fileName) 209 | 210 | # File content has changed 211 | if next != previous: 212 | echo "Code changed!" 213 | previous = next 214 | 215 | # If process is found, kill it and re-compile 216 | if getProcessId(port, pid): 217 | killProcess(pid) 218 | (output, exitCode) = nimCompile(fileName) 219 | if exitCode == 1: 220 | echo output 221 | else: 222 | port = getPort() 223 | createThread(app, appProc, execName) 224 | 225 | else: 226 | port = getPort() 227 | createThread(app, appProc, execName) 228 | 229 | sleep(200) 230 | 231 | of "update": 232 | updateXander() 233 | 234 | else: 235 | echo "Bad command! Commands are 'new', 'run' nd 'listen'." -------------------------------------------------------------------------------- /src/xander/constants.nim: -------------------------------------------------------------------------------- 1 | import 2 | regex 3 | 4 | const # Characters 5 | newLine*: string = "\n" 6 | tab*: string = " " 7 | 8 | const # Template tags 9 | contentTag*: string = "{[%content%]}" 10 | 11 | const # Request handler string 12 | requestHandlerString*: string = "proc(request: Request, data: var Data, headers: var HttpHeaders, cookies: var Cookies, session: var Session, files: var UploadFiles): Response {.gcsafe.} =" 13 | 14 | const # Time 15 | unixEpoch*: string = "Thu, 1 Jan 1970 12:00:00 UTC" -------------------------------------------------------------------------------- /src/xander/contenttype.nim: -------------------------------------------------------------------------------- 1 | import 2 | strutils 3 | 4 | func getContentType*(ext: string, charset = "UTF-8"): string = 5 | var ext = ext 6 | if not(startsWith(ext, '.')): 7 | ext = '.' & ext 8 | result = case ext: 9 | of ".aac": 10 | "audio/aac" 11 | of ".abw": 12 | "application/x-abiword" 13 | of ".arc": 14 | "application/x-freearc" 15 | of ".avi": 16 | "video/x-msvideo" 17 | of ".azw": 18 | "application/vnd.amazon.ebook" 19 | of ".bin": 20 | "application/octet-stream" 21 | of ".bmp": 22 | "image/bmp" 23 | of ".bz": 24 | "application/x-bzip" 25 | of ".bz2": 26 | "application/x-bzip2" 27 | of ".csh": 28 | "application/x-csh" 29 | of ".css": 30 | "text/css" 31 | of ".csv": 32 | "text/csv" 33 | of ".doc": 34 | "application/msword" 35 | of ".docx": 36 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" 37 | of ".eot": 38 | "application/vnd.ms-fontobject" 39 | of ".epub": 40 | "application/epub+zip" 41 | of ".gif": 42 | "image/gif" 43 | of ".html": 44 | "text/html" 45 | of ".htm": 46 | "text/html" 47 | of ".ico": 48 | "image/vnd.microsoft.icon" 49 | of ".ics": 50 | "text/calendar" 51 | of ".jar": 52 | "application/java-archive" 53 | of ".jpg": 54 | "image/jpeg" 55 | of ".jpeg": 56 | "image/jpeg" 57 | of ".js": 58 | "text/javascript" 59 | of ".json": 60 | "application/json" 61 | of ".jsonld": 62 | "application/ld+json" 63 | of ".midi": 64 | "audio/midi audio/x-midi" 65 | of ".mid": 66 | "audio/midi audio/x-midi" 67 | of ".mjs": 68 | "text/javascript" 69 | of ".mp3": 70 | "audio/mpeg" 71 | of ".mpeg": 72 | "video/mpeg" 73 | of ".mpkg": 74 | "application/vnd.apple.installer+xml" 75 | of ".odp": 76 | "application/vnd.oasis.opendocument.presentation" 77 | of ".ods": 78 | "application/vnd.oasis.opendocument.spreadsheet" 79 | of ".odt": 80 | "application/vnd.oasis.opendocument.text" 81 | of ".oga": 82 | "audio/ogg" 83 | of ".ogv": 84 | "video/ogg" 85 | of ".ogx": 86 | "application/ogg" 87 | of ".otf": 88 | "font/otf" 89 | of ".png": 90 | "image/png" 91 | of ".pdf": 92 | "application/pdf" 93 | of ".ppt": 94 | "application/vnd.ms-powerpoint" 95 | of ".pptx": 96 | "application/vnd.openxmlformats-officedocument.presentationml.presentation" 97 | of ".rar": 98 | "application/x-rar-compressed" 99 | of ".rtf": 100 | "application/rtf" 101 | of ".sh": 102 | "application/x-sh" 103 | of ".svg": 104 | "image/svg+xml" 105 | of ".swf": 106 | "application/x-shockwave-flash" 107 | of ".tar": 108 | "application/x-tar" 109 | of ".tiff": 110 | "image/tiff" 111 | of ".tif": 112 | "image/tiff" 113 | of ".ts": 114 | "video/mp2t" 115 | of ".ttf": 116 | "font/ttf" 117 | of ".txt": 118 | "text/plain" 119 | of ".vsd": 120 | "application/vnd.visio" 121 | of ".wav": 122 | "audio/wav" 123 | of ".weba": 124 | "audio/webm" 125 | of ".webm": 126 | "video/webm" 127 | of ".webp": 128 | "image/webp" 129 | of ".woff": 130 | "font/woff" 131 | of ".woff2": 132 | "font/woff2" 133 | of ".xhtml": 134 | "application/xhtml+xml" 135 | of ".xls": 136 | "application/vnd.ms-excel" 137 | of ".xlsx": 138 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 139 | of ".xml": 140 | "application/xml" 141 | of ".xul": 142 | "application/vnd.mozilla.xul+xml" 143 | of ".zip": 144 | "application/zip" 145 | of ".3gp": 146 | "video/3gpp" 147 | of ".3g2": 148 | "video/3gpp2" 149 | of ".7z": 150 | "application/x-7z-compressed" 151 | else: 152 | "text/plain" 153 | return result & "; charset=" & charset -------------------------------------------------------------------------------- /src/xander/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Colors 3 | GREEN='\033[0;32m' 4 | WHITE='\033[1;37m' 5 | NC='\033[0m' # No Color 6 | # Make .xander directory 7 | [ -d "~/.xander" ] && mkdir "~/.xander" 8 | # Get latest version of Xander 9 | x=($(ls ~/.nimble/pkgs | grep xander-)) 10 | y=~/.nimble/pkgs/${x[-1]}/xander.nim 11 | # Compiling with cpp for better compatiblity 12 | echo "Found Xander in $y" 13 | echo -e "${WHITE}Compiling ${x[-1]}${NC}" 14 | ~/.nimble/bin/nim cpp --verbosity:1 --hints:off --threads:on -o:~/.xander/xander $y 15 | echo -e "${GREEN}Finished!${NC} The Xander executable can be found in ~/.xander" 16 | echo "Add the following line to ~/.bashrc or ~/.profile:" 17 | echo "" 18 | echo " export PATH=/home/$USER/.xander:\$PATH" 19 | echo "" -------------------------------------------------------------------------------- /src/xander/tools.nim: -------------------------------------------------------------------------------- 1 | import 2 | strutils 3 | 4 | func quote*(s: string): string = 5 | "\"" & s & "\"" 6 | 7 | func unquote*(s: string): string = 8 | s.replace("\"", "") 9 | 10 | func unquote*(n: NimNode): string = 11 | repr(n).replace("\"", "") 12 | 13 | func toIntSeq*(s: string, sep = ','): seq[int] = 14 | result = newSeq[int]() 15 | for value in s.multiReplace(("[", ""), ("]", "")).split(sep): 16 | result.add(parseInt(value)) 17 | 18 | func indentation*(s: string): int = 19 | for c in s: 20 | if c == ' ': 21 | result += 1 22 | else: 23 | break 24 | -------------------------------------------------------------------------------- /src/xander/types.nim: -------------------------------------------------------------------------------- 1 | import 2 | asyncdispatch, 3 | asynchttpserver, 4 | json, 5 | regex, 6 | sequtils, 7 | strutils, 8 | tables, 9 | macros 10 | 11 | import 12 | constants, 13 | tools 14 | 15 | type 16 | Cookie* = tuple 17 | name: string 18 | value: string 19 | domain: string 20 | path: string 21 | expires: string 22 | secure: bool 23 | httpOnly: bool 24 | 25 | func newCookie*(name, value, domain, path, expires = "", secure, httpOnly = false): Cookie = 26 | return (name, value, domain, path, expires, secure, httpOnly) 27 | 28 | type 29 | Cookies* = tuple 30 | client: Table[string, Cookie] 31 | server: Table[string, Cookie] 32 | 33 | func newCookies*(): Cookies = 34 | var c: Cookies 35 | c.client = initTable[string, Cookie]() 36 | c.server = initTable[string, Cookie]() 37 | return c 38 | 39 | func get*(cookies: Cookies, cookieName: string): string = 40 | if cookies.client.hasKey(cookieName): 41 | return cookies.client[cookieName].value 42 | 43 | func set*(cookies: var Cookies, c: Cookie): void = 44 | let cookieName = c.name 45 | cookies.server[cookieName] = c 46 | 47 | func set*(cookies: var Cookies, name, value, domain, path, expires = "", secure, httpOnly = false): void = 48 | cookies.server[name] = newCookie(name, value, domain, path, expires, secure, httpOnly) 49 | 50 | func contains*(cookies: var Cookies, cookieName: string): bool = 51 | cookies.client.hasKey(cookieName) 52 | 53 | func contains*(cookies: Cookies, keys: varargs[string]): bool = 54 | result = true 55 | for key in keys: 56 | if not cookies.client.hasKey(key): 57 | return false 58 | 59 | func containsAndEquals*(cookies: var Cookies, cookieName, value: string): bool = 60 | cookies.contains(cookieName) and cookies.get(cookieName) == value 61 | 62 | func remove*(cookies: var Cookies, cookieName: string): void = 63 | cookies.server[cookieName] = newCookie(cookieName, expires = unixEpoch) 64 | 65 | # Cookies in the request header only contain the 'name' and 'value' -fields 66 | func setClient*(cookies: var Cookies, name, value: string = ""): void = 67 | cookies.client[name] = newCookie(name, value) 68 | 69 | type 70 | Data* = JsonNode 71 | 72 | func newData*(): Data = 73 | newJObject() 74 | 75 | func newData*[T](key: string, value: T): Data = 76 | result = newData() 77 | set(result, key, value) 78 | 79 | func get*(data: Data, key: string): string = 80 | let d = data.getOrDefault(key) 81 | if d != nil: 82 | return d.getStr() 83 | 84 | macro withData*(keys: varargs[string], body: untyped): void = 85 | var vars = repr(keys).strip(chars = {'[', ']'}).replace("\"", "").split(", ") 86 | var source: string = "if" 87 | # create if statement 88 | for i in 0 .. vars.len - 1: 89 | source &= " data.hasKey($1)".format(tools.quote(vars[i])) 90 | if i == vars.len - 1: source &= ":\n" 91 | else: source &= " and" 92 | # add var declaration(s) 93 | for v in vars: 94 | #TODO: source &= "if data[$1].kind == JArray:\n\t" 95 | source &= tab & "var $1 = data.get($2)\n".format(v.replace('-', '_'), tools.quote(v)) 96 | # add body 97 | for line in repr(body).splitLines: 98 | if line.len > 0: 99 | source &= tab & line & newLine 100 | # parse statement 101 | parseStmt(source) 102 | 103 | macro withHeaders*(keys: varargs[string], body: untyped): void = 104 | var vars = repr(keys).strip(chars = {'[', ']'}).replace("\"", "").split(", ") 105 | var source: string = "if" 106 | # create if statement 107 | for i in 0 .. vars.len - 1: 108 | source &= " request.headers.hasKey($1)".format(tools.quote(vars[i])) 109 | if i == vars.len - 1: source &= ":\n" 110 | else: source &= " and" 111 | # add var declaration(s) 112 | for v in vars: 113 | source &= tab & "var $1 = request.headers[$2]\n".format(v.replace('-', '_'), tools.quote(v)) 114 | # add body 115 | for line in repr(body).splitLines: 116 | if line.len > 0: 117 | source &= tab & line & newLine 118 | # parse statement 119 | parseStmt(source) 120 | 121 | func getBool*(data: Data, key: string): bool = 122 | let d = data.getOrDefault(key) 123 | if d != nil: 124 | return d.getBool() 125 | 126 | func getInt*(data: Data, key: string): int = 127 | let d = data.getOrDefault(key) 128 | if d != nil: 129 | return d.getInt() 130 | 131 | func set*[T](node: var Data, key: string, data: T) = 132 | add(node, key, %data) 133 | 134 | func add*[T](node: Data, key: string, value: T): Data = 135 | result = node 136 | add(node, key, %value) 137 | 138 | func hasKeys*(data: Data, keys: varargs[string]): bool = 139 | result = true 140 | for key in keys: 141 | if not data.hasKey(key): 142 | return false 143 | 144 | func `[]=`*[T](node: var Data, key: string, value: T) = 145 | add(node, key, %value) 146 | 147 | type 148 | Dictionary* = 149 | Table[string, string] 150 | 151 | func newDictionary*(): Dictionary = 152 | return initTable[string, string]() 153 | 154 | func set*(dict: var Dictionary, key, value: string) = 155 | dict[key] = value 156 | 157 | type 158 | Response* = tuple 159 | body: string 160 | httpCode: HttpCode 161 | headers: HttpHeaders 162 | 163 | type 164 | Session* = Data 165 | Sessions* = Table[string, Session] 166 | 167 | func newSession*(): Session = 168 | newData() 169 | 170 | func newSessions*(): Sessions = 171 | initTable[string, Session]() 172 | 173 | func remove*(session: var Session, key: string): void = 174 | if session.hasKey(key): 175 | delete(session, key) 176 | 177 | type 178 | UploadFile* = tuple 179 | name, ext, content: string 180 | size: int 181 | 182 | func newUploadFile*(name, ext, content: string, size: int = 0): UploadFile = 183 | (name, ext, content, size) 184 | 185 | type 186 | UploadFiles* = 187 | Table[string, seq[UploadFile]] 188 | 189 | func newUploadFiles*(): UploadFiles = 190 | return initTable[string, seq[UploadFile]]() 191 | 192 | type 193 | RequestHandlerVariables* = tuple 194 | data: Data 195 | headers: HttpHeaders 196 | cookies: Cookies 197 | session: Session 198 | files: UploadFiles 199 | 200 | func newRequestHandlerVariables*(): RequestHandlerVariables = 201 | (newData(), newHttpHeaders(), newCookies(), newSession(), newUploadFiles()) 202 | 203 | type 204 | RequestHandler* = 205 | proc(request: Request, data: var Data, headers: var HttpHeaders, cookies: var Cookies, session: var Session, files: var UploadFiles): Response {.gcsafe.} 206 | # TODO: Would below work? 207 | #proc(request: Request, X: var RequestHandlerVariables): Response {.gcsafe.} 208 | 209 | template newRequestHandler*(body: untyped): RequestHandler = 210 | body 211 | 212 | type 213 | ServerRoute* = tuple 214 | route: string 215 | handler: RequestHandler 216 | 217 | func newServerRoute*(route: string, handler: RequestHandler): ServerRoute = 218 | (route, handler) 219 | 220 | type 221 | Domain* = 222 | # HttpMethod => List of Server Routes 223 | Table[HttpMethod, seq[ServerRoute]] 224 | Domains* = 225 | Table[string, Domain] 226 | 227 | func newDomain*(): Domain = 228 | initTable[HttpMethod, seq[ServerRoute]]() 229 | 230 | func newDomains*(): Domains = 231 | initTable[string, Domain]() 232 | 233 | type 234 | Hosts* = 235 | # Host => Domains 236 | Table[string, Domains] 237 | 238 | const defaultHost* = "DEFAULT_HOST" 239 | const defaultDomain* = "DEFAULT_DOMAIN" 240 | const defaultMethod* = HttpGet 241 | 242 | func `$`*(server: Hosts): string = 243 | result = newLine & newLine & "~~~ SERVER STRUCTURE ~~~" & newLine 244 | for host in server.keys: 245 | result &= " HOST " & host & newLine 246 | for domain in server[host].keys: 247 | result &= " DOMAIN " & domain & newLine 248 | for httpMethod in server[host][domain].keys: 249 | result &= " METHOD " & $httpMethod & newLine 250 | for route in server[host][domain][httpMethod]: 251 | result &= " ROUTE " & route.route & newLine 252 | result &= "~~~ SERVER STRUCTURE ~~~" & newLine & newLine 253 | 254 | func newHosts*(): Hosts = 255 | initTable[string, Domains]() 256 | 257 | func existsHost*(server: Hosts, host: string): bool = 258 | server.hasKey(host) 259 | 260 | func existsDomain*(server: Hosts, domain: string, host: string): bool = 261 | server.existsHost(host) and server[host].hasKey(domain) 262 | 263 | func existsMethod*(server: Hosts, httpMethod: HttpMethod, host = defaultHost, domain = defaultDomain): bool = 264 | server.existsHost(host) and server.existsDomain(domain, host) and server[host][domain].hasKey(httpMethod) 265 | 266 | func addHost*(server: var Hosts, host = defaultHost): void = 267 | if not server.hasKey(host): 268 | server[host] = newDomains() 269 | 270 | func addDomain*(server: var Hosts, domain = defaultDomain, host = defaultHost): void = 271 | server[host][domain] = newDomain() 272 | 273 | func addMethod*(server: var Hosts, httpMethod = defaultMethod, host = defaultHost, domain = defaultDomain): void = 274 | if server.hasKey(host) and server[host].hasKey(domain): 275 | server[host][domain][httpMethod] = newSeq[ServerRoute]() 276 | 277 | func getRoutes*(server: Hosts, host = defaultHost, domain = defaultDomain, httpMethod = defaultMethod): seq[ServerRoute] = 278 | server[host][domain][httpMethod] 279 | 280 | func addRoute*(server: var Hosts, httpMethod: HttpMethod, route: string, handler: RequestHandler, host = defaultHost, domain = defaultDomain): void = 281 | if not server.existsHost(host): 282 | server.addHost(host) 283 | if not server.existsDomain(domain, host): 284 | server.addDomain(domain, host) 285 | if not server.existsMethod(httpMethod, host, domain): 286 | server.addMethod(httpMethod, host, domain) 287 | var routes = server.getRoutes(host, domain, httpMethod) 288 | routes.add newServerRoute(route, handler) 289 | server[host][domain][httpMethod] = routes 290 | 291 | func addRoute*(server: var Hosts, route: string, handler: RequestHandler, host = defaultHost, domain = defaultDomain, httpMethod: HttpMethod = defaultMethod): void = 292 | var routes = server.getRoutes(host, domain, httpMethod) 293 | routes.add newServerRoute(route, handler) 294 | server[host][domain][httpMethod] = routes 295 | 296 | proc isDefaultHost*(server: Hosts): bool = 297 | server.existsHost(defaultHost) #and server.len == 1 298 | 299 | type 300 | ErrorHandler* = 301 | proc(request: Request, httpCode: HttpCode): Response {.gcsafe.} -------------------------------------------------------------------------------- /src/xander/ws.nim: -------------------------------------------------------------------------------- 1 | # !! 2 | # All credit to treeforms's websocket library 3 | # https://github.com/treeform/ws 4 | # !! 5 | 6 | import httpcore, httpclient, asynchttpserver, asyncdispatch, nativesockets, 7 | asyncnet, strutils, streams, random, std/sha1, base64, uri, strformat, httpcore 8 | 9 | type 10 | ReadyState* = enum 11 | Connecting = 0 # The connection is not yet open. 12 | Open = 1 # The connection is open and ready to communicate. 13 | Closing = 2 # The connection is in the process of closing. 14 | Closed = 3 # The connection is closed or couldn't be opened. 15 | 16 | WebSocket* = ref object 17 | req*: Request 18 | version*: int 19 | key*: string 20 | protocol*: string 21 | readyState*: ReadyState 22 | 23 | WebSocketError* = object of Exception 24 | 25 | 26 | template `[]`(value: uint8, index: int): bool = 27 | ## Get bits from uint8, uint8[2] gets 2nd bit. 28 | (value and (1 shl (7 - index))) != 0 29 | 30 | 31 | proc nibbleFromChar(c: char): int = 32 | ## Converts hex chars like `0` to 0 and `F` to 15. 33 | case c: 34 | of '0'..'9': (ord(c) - ord('0')) 35 | of 'a'..'f': (ord(c) - ord('a') + 10) 36 | of 'A'..'F': (ord(c) - ord('A') + 10) 37 | else: 255 38 | 39 | 40 | proc nibbleToChar(value: int): char = 41 | ## Converts number like 0 to `0` and 15 to `fg`. 42 | case value: 43 | of 0..9: char(value + ord('0')) 44 | else: char(value + ord('a') - 10) 45 | 46 | 47 | proc decodeBase16*(str: string): string = 48 | ## Base16 decode a string. 49 | result = newString(str.len div 2) 50 | for i in 0 ..< result.len: 51 | result[i] = chr( 52 | (nibbleFromChar(str[2 * i]) shl 4) or 53 | nibbleFromChar(str[2 * i + 1])) 54 | 55 | 56 | proc encodeBase16*(str: string): string = 57 | ## Base61 encode a string. 58 | result = newString(str.len * 2) 59 | for i, c in str: 60 | result[i * 2] = nibbleToChar(ord(c) shr 4) 61 | result[i * 2 + 1] = nibbleToChar(ord(c) and 0x0f) 62 | 63 | 64 | proc genMaskKey*(): array[4, char] = 65 | ## Generates a random key of 4 random chars. 66 | proc r(): char = char(rand(256)) 67 | [r(), r(), r(), r()] 68 | 69 | 70 | proc newWebSocket*(req: Request): Future[WebSocket] {.async.} = 71 | ## Creates a new socket from a request. 72 | if not req.headers.hasKey("sec-websocket-version"): 73 | await req.respond(Http404, "Not Found") 74 | raise newException(WebSocketError, "Not a valid websocket handshake.") 75 | 76 | var ws = WebSocket() 77 | ws.req = req 78 | ws.version = parseInt(req.headers["sec-webSocket-version"]) 79 | ws.key = req.headers["sec-webSocket-key"].strip() 80 | if req.headers.hasKey("sec-webSocket-protocol"): 81 | ws.protocol = req.headers["sec-websocket-protocol"].strip() 82 | 83 | let sh = secureHash(ws.key & "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") 84 | let acceptKey = base64.encode(decodeBase16($sh)) 85 | 86 | var responce = "HTTP/1.1 101 Web Socket Protocol Handshake\c\L" 87 | responce.add("Sec-WebSocket-Accept: " & acceptKey & "\c\L") 88 | responce.add("Connection: Upgrade\c\L") 89 | responce.add("Upgrade: webSocket\c\L") 90 | if not ws.protocol.len == 0: 91 | responce.add("Sec-WebSocket-Protocol: " & ws.protocol & "\c\L") 92 | responce.add "\c\L" 93 | 94 | await ws.req.client.send(responce) 95 | ws.readyState = Open 96 | return ws 97 | 98 | 99 | proc newWebSocket*(url: string, protocol: string = ""): Future[WebSocket] {.async.} = 100 | ## Creates a new WebSocket connection, protocol is optinal, "" means no protocol. 101 | var ws = WebSocket() 102 | ws.req = Request() 103 | ws.req.client = newAsyncSocket() 104 | ws.protocol = protocol 105 | 106 | var uri = parseUri(url) 107 | var port = Port(9001) 108 | case uri.scheme 109 | of "wss": 110 | uri.scheme = "https" 111 | port = Port(443) 112 | of "ws": 113 | uri.scheme = "http" 114 | port = Port(80) 115 | else: 116 | raise newException(WebSocketError, 117 | &"Scheme {uri.scheme} not supported yet.") 118 | if uri.port.len > 0: 119 | port = Port(parseInt(uri.port)) 120 | 121 | var client = newAsyncHttpClient() 122 | client.headers = newHttpHeaders({ 123 | "Connection": "Upgrade", 124 | "Upgrade": "websocket", 125 | "Sec-WebSocket-Version": "13", 126 | "Sec-WebSocket-Key": "JCSoP2Cyk0cHZkKAit5DjA==", 127 | "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits" 128 | }) 129 | if ws.protocol != "": 130 | client.headers["Sec-WebSocket-Protocol"] = ws.protocol 131 | var _ = await client.get(url) 132 | ws.req.client = client.getSocket() 133 | 134 | ws.readyState = Open 135 | return ws 136 | 137 | 138 | type 139 | Opcode* = enum 140 | ## 4 bits. Defines the interpretation of the "Payload data". 141 | Cont = 0x0 ## denotes a continuation frame 142 | Text = 0x1 ## denotes a text frame 143 | Binary = 0x2 ## denotes a binary frame 144 | # 3-7 are reserved for further non-control frames 145 | Close = 0x8 ## denotes a connection close 146 | Ping = 0x9 ## denotes a ping 147 | Pong = 0xa ## denotes a pong 148 | # B-F are reserved for further control frames 149 | 150 | #[ 151 | 0 1 2 3 152 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 153 | +-+-+-+-+-------+-+-------------+-------------------------------+ 154 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 155 | |I|S|S|S| (4) |A| (7) | (16/64) | 156 | |N|V|V|V| |S| | (if payload len==126/127) | 157 | | |1|2|3| |K| | | 158 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 159 | | Extended payload length continued, if payload len == 127 | 160 | + - - - - - - - - - - - - - - - +-------------------------------+ 161 | | |Masking-key, if MASK set to 1 | 162 | +-------------------------------+-------------------------------+ 163 | | Masking-key (continued) | Payload Data | 164 | +-------------------------------- - - - - - - - - - - - - - - - + 165 | : Payload Data continued ... : 166 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 167 | | Payload Data continued ... | 168 | +---------------------------------------------------------------+ 169 | ]# 170 | Frame* = tuple 171 | fin: bool ## Indicates that this is the final fragment in a message. 172 | rsv1: bool ## MUST be 0 unless negotiated that defines meanings 173 | rsv2: bool 174 | rsv3: bool 175 | opcode: Opcode ## Defines the interpretation of the "Payload data". 176 | mask: bool ## Defines whether the "Payload data" is masked. 177 | data: string ## Payload data 178 | 179 | 180 | proc encodeFrame*(f: Frame): string = 181 | ## Encodes a frame into a string buffer. 182 | ## See https://tools.ietf.org/html/rfc6455#section-5.2 183 | 184 | var ret = newStringStream() 185 | 186 | var b0 = (f.opcode.uint8 and 0x0f) # 0th byte: opcodes and flags 187 | if f.fin: 188 | b0 = b0 or 128u8 189 | 190 | ret.write(b0) 191 | 192 | # Payload length can be 7 bits, 7+16 bits, or 7+64 bits. 193 | # 1st byte: playload len start and mask bit. 194 | var b1 = 0u8 195 | 196 | if f.data.len <= 125: 197 | b1 = f.data.len.uint8 198 | elif f.data.len > 125 and f.data.len <= 0xffff: 199 | b1 = 126u8 200 | else: 201 | b1 = 127u8 202 | 203 | if f.mask: 204 | b1 = b1 or (1 shl 7) 205 | 206 | ret.write(uint8 b1) 207 | 208 | # Only need more bytes if data len is 7+16 bits, or 7+64 bits. 209 | if f.data.len > 125 and f.data.len <= 0xffff: 210 | # Data len is 7+16 bits. 211 | ret.write(htons(f.data.len.uint16)) 212 | elif f.data.len > 0xffff: 213 | # Data len is 7+64 bits. 214 | var len = f.data.len 215 | ret.write char((len shr 56) and 255) 216 | ret.write char((len shr 48) and 255) 217 | ret.write char((len shr 40) and 255) 218 | ret.write char((len shr 32) and 255) 219 | ret.write char((len shr 24) and 255) 220 | ret.write char((len shr 16) and 255) 221 | ret.write char((len shr 8) and 255) 222 | ret.write char(len and 255) 223 | 224 | var data = f.data 225 | 226 | if f.mask: 227 | # If we need to maks it generate random mask key and mask the data. 228 | let maskKey = genMaskKey() 229 | for i in 0..= 0) 225 | 226 | var z: ZStream 227 | var windowBits = MAX_WBITS 228 | case (stream) 229 | of RAW_DEFLATE: windowBits = -MAX_WBITS 230 | of GZIP_STREAM: windowBits = MAX_WBITS + 16 231 | of ZLIB_STREAM, DETECT_STREAM: 232 | discard # DETECT_STREAM defaults to ZLIB_STREAM 233 | 234 | var status = deflateInit2(z, level.int32, Z_DEFLATED.int32, 235 | windowBits.int32, Z_MEM_LEVEL.int32, 236 | Z_DEFAULT_STRATEGY.int32) 237 | case status 238 | of Z_OK: discard 239 | of Z_MEM_ERROR: raise newException(OutOfMemError, "") 240 | of Z_STREAM_ERROR: raise newException(ZlibStreamError, "invalid zlib stream parameter!") 241 | of Z_VERSION_ERROR: raise newException(ZlibStreamError, "zlib version mismatch!") 242 | else: raise newException(ZlibStreamError, "Unkown error(" & $status & ") : " & $z.msg) 243 | 244 | let space = deflateBound(z, sourceLen.Ulong) 245 | var compressed = newString(space) 246 | z.next_in = sourceBuf 247 | z.avail_in = sourceLen.Uint 248 | z.next_out = addr(compressed[0]) 249 | z.avail_out = space.Uint 250 | 251 | status = deflate(z, Z_FINISH) 252 | if status != Z_STREAM_END: 253 | discard deflateEnd(z) # cleanup allocated ressources 254 | raise newException(ZlibStreamError, "Invalid stream state(" & $status & ") : " & $z.msg) 255 | 256 | status = deflateEnd(z) 257 | if status != Z_OK: # cleanup allocated ressources 258 | raise newException(ZlibStreamError, "Invalid stream state(" & $status & ") : " & $z.msg) 259 | 260 | compressed.setLen(z.total_out) 261 | swap(result, compressed) 262 | 263 | proc compress*(input: string; level=Z_DEFAULT_COMPRESSION; stream=GZIP_STREAM): string = 264 | ## Given a string, returns its deflated version with an optional header. 265 | ## 266 | ## Valid arguments for ``stream`` are 267 | ## - ``ZLIB_STREAM`` - add a zlib header and footer. 268 | ## - ``GZIP_STREAM`` - add a basic gzip header and footer. 269 | ## - ``RAW_DEFLATE`` - no header is generated. 270 | ## 271 | ## Compression level can be set with ``level`` argument. Currently 272 | ## ``Z_DEFAULT_COMPRESSION`` is 6. 273 | ## 274 | ## Returns "" on failure. 275 | result = compress(input, input.len, level, stream) 276 | 277 | proc uncompress*(sourceBuf: cstring, sourceLen: Natural; stream=DETECT_STREAM): string = 278 | ## Given a deflated buffer returns its inflated content as a string. 279 | ## 280 | ## Valid arguments for ``stream`` are 281 | ## - ``DETECT_STREAM`` - detect if zlib or gzip header is present 282 | ## and decompress stream. Fail on raw deflate stream. 283 | ## - ``ZLIB_STREAM`` - decompress a zlib stream. 284 | ## - ``GZIP_STREAM`` - decompress a gzip stream. 285 | ## - ``RAW_DEFLATE`` - decompress a raw deflate stream. 286 | ## 287 | ## Passing a nil cstring will crash this proc in release mode and assert in 288 | ## debug mode. 289 | ## 290 | ## Returns "" on problems. Failure is a very loose concept, it could be you 291 | ## passing a non deflated string, or it could mean not having enough memory 292 | ## for the inflated version. 293 | ## 294 | ## The uncompression algorithm is based on http://zlib.net/zpipe.c. 295 | assert(not sourceBuf.isNil) 296 | assert(sourceLen >= 0) 297 | var z: ZStream 298 | var decompressed: string = "" 299 | var sbytes = 0 300 | var wbytes = 0 301 | ## allocate inflate state 302 | 303 | z.availIn = 0 304 | var wbits = case (stream) 305 | of RAW_DEFLATE: -MAX_WBITS 306 | of ZLIB_STREAM: MAX_WBITS 307 | of GZIP_STREAM: MAX_WBITS + 16 308 | of DETECT_STREAM: MAX_WBITS + 32 309 | 310 | var status = inflateInit2(z, wbits.int32) 311 | 312 | case status 313 | of Z_OK: discard 314 | of Z_MEM_ERROR: raise newException(OutOfMemError, "") 315 | of Z_STREAM_ERROR: raise newException(ZlibStreamError, "invalid zlib stream parameter!") 316 | of Z_VERSION_ERROR: raise newException(ZlibStreamError, "zlib version mismatch!") 317 | else: raise newException(ZlibStreamError, "Unkown error(" & $status & ") : " & $z.msg) 318 | 319 | # run loop until all input is consumed. 320 | # handle concatenated deflated stream with header. 321 | while true: 322 | z.availIn = (sourceLen - sbytes).int32 323 | 324 | # no more input available 325 | if (sourceLen - sbytes) <= 0: break 326 | z.nextIn = sourceBuf[sbytes].unsafeaddr 327 | 328 | # run inflate() on available input until output buffer is full 329 | while true: 330 | # if written bytes >= output size : resize output 331 | if wbytes >= decompressed.len: 332 | let cur_outlen = decompressed.len 333 | let new_outlen = if decompressed.len == 0: sourceLen*2 else: decompressed.len*2 334 | if new_outlen < cur_outlen: # unsigned integer overflow, buffer too large 335 | discard inflateEnd(z); 336 | raise newException(OverflowError, "zlib stream decompressed size is too large! (size > " & $int.high & ")") 337 | 338 | decompressed.setLen(new_outlen) 339 | 340 | # available space for decompression 341 | let space = decompressed.len - wbytes 342 | z.availOut = space.Uint 343 | z.nextOut = decompressed[wbytes].addr 344 | 345 | status = inflate(z, Z_NO_FLUSH) 346 | if status.int8 notin {Z_OK.int8, Z_STREAM_END.int8, Z_BUF_ERROR.int8}: 347 | discard inflateEnd(z) 348 | case status 349 | of Z_MEM_ERROR: raise newException(OutOfMemError, "") 350 | of Z_DATA_ERROR: raise newException(ZlibStreamError, "invalid zlib stream parameter!") 351 | else: raise newException(ZlibStreamError, "Unkown error(" & $status & ") : " & $z.msg) 352 | 353 | # add written bytes, if any. 354 | wbytes += space - z.availOut.int 355 | 356 | # may need more input 357 | if not (z.availOut == 0): break 358 | 359 | # inflate() says stream is done 360 | if (status == Z_STREAM_END): 361 | # may have another stream concatenated 362 | if z.availIn != 0: 363 | sbytes = sourceLen - z.availIn # add consumed bytes 364 | if inflateReset(z) != Z_OK: # reset zlib struct and try again 365 | raise newException(ZlibStreamError, "Invalid stream state(" & $status & ") : " & $z.msg) 366 | else: 367 | break # end of decompression 368 | 369 | # clean up and don't care about any error 370 | discard inflateEnd(z) 371 | 372 | if status != Z_STREAM_END: 373 | raise newException(ZlibStreamError, "Invalid stream state(" & $status & ") : " & $z.msg) 374 | 375 | decompressed.setLen(wbytes) 376 | swap(result, decompressed) 377 | 378 | 379 | proc uncompress*(sourceBuf: string; stream=DETECT_STREAM): string = 380 | ## Given a GZIP-ed string return its inflated content. 381 | ## 382 | ## Valid arguments for ``stream`` are 383 | ## - ``DETECT_STREAM`` - detect if zlib or gzip header is present 384 | ## and decompress stream. Fail on raw deflate stream. 385 | ## - ``ZLIB_STREAM`` - decompress a zlib stream. 386 | ## - ``GZIP_STREAM`` - decompress a gzip stream. 387 | ## - ``RAW_DEFLATE`` - decompress a raw deflate stream. 388 | ## 389 | ## Returns "" on failure. 390 | result = uncompress(sourceBuf, sourceBuf.len, stream) 391 | 392 | 393 | 394 | proc deflate*(buffer: var string; level=Z_DEFAULT_COMPRESSION; stream=GZIP_STREAM): bool {.discardable.} = 395 | ## Convenience proc which deflates a string and insert an optional header/footer. 396 | ## 397 | ## Valid arguments for ``stream`` are 398 | ## - ``ZLIB_STREAM`` - add a zlib header and footer. 399 | ## - ``GZIP_STREAM`` - add a basic gzip header and footer. 400 | ## - ``RAW_DEFLATE`` - no header is generated. 401 | ## 402 | ## Compression level can be set with ``level`` argument. Currently 403 | ## ``Z_DEFAULT_COMPRESSION`` is 6. 404 | ## 405 | ## Returns true if `buffer` was successfully deflated otherwise the buffer is untouched. 406 | 407 | var temp = compress(addr(buffer[0]), buffer.len, level, stream) 408 | if temp.len != 0: 409 | swap(buffer, temp) 410 | result = true 411 | 412 | proc inflate*(buffer: var string; stream=DETECT_STREAM): bool {.discardable.} = 413 | ## Convenience proc which inflates a string containing compressed data 414 | ## with an optional header. 415 | ## 416 | ## Valid argument for ``stream`` are: 417 | ## - ``DETECT_STREAM`` - detect if zlib or gzip header is present 418 | ## and decompress stream. Fail on raw deflate stream. 419 | ## - ``ZLIB_STREAM`` - decompress a zlib stream. 420 | ## - ``GZIP_STREAM`` - decompress a gzip stream. 421 | ## - ``RAW_DEFLATE`` - decompress a raw deflate stream. 422 | ## 423 | ## It is ok to pass a buffer which doesn't contain deflated data, 424 | ## in this case the proc won't modify the buffer. 425 | ## 426 | ## Returns true if `buffer` was successfully inflated. 427 | 428 | var temp = uncompress(addr(buffer[0]), buffer.len, stream) 429 | if temp.len != 0: 430 | swap(buffer, temp) 431 | result = true 432 | -------------------------------------------------------------------------------- /xander.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" 4 | author = "Santeri Sydänmetsä" 5 | description = "A Nim web application framework" 6 | license = "MIT" 7 | 8 | # Deps 9 | 10 | requires "nim >= 0.20.0" 11 | srcDir = "src" --------------------------------------------------------------------------------