├── LICENSE ├── README.md ├── examples └── server.nim ├── httpform.nim ├── httpform.nimble ├── incl.nim ├── multipart.nim └── tests ├── appjson.nim ├── multipart.nim ├── octetstream.nim ├── unknown.nim └── urlencoded.nim /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 WangTong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HttpForm, The Http Request Form Parser 2 | ========================================== 3 | 4 | This module was developed for submit form by http protocol, upload and encoding images and videos. 5 | 6 | Example 7 | -------- 8 | 9 | Upload files with **HTML5 `
31 | 32 | """ 33 | 34 | proc cb(req: Request) {.async.} = 35 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 36 | if req.reqMethod.toLower() == "get": 37 | await req.respond(Http200, html) 38 | else: 39 | # retrieve sent field and files 40 | echo fields["name"] 41 | echo files["upload"][0]["path"] 42 | echo files["upload"][0]["size"] 43 | echo files["upload"][0]["type"] 44 | echo files["upload"][0]["name"] 45 | 46 | await req.respond(Http200, "hello") 47 | waitFor server.serve(Port(8000), cb) 48 | 49 | main() 50 | ``` 51 | 52 | Usage 53 | ------- 54 | 55 | `$ nimble init` make a .nimble file, and edit: 56 | 57 | ``` 58 | [Package] 59 | name = "app" 60 | version = "0.1.0" 61 | author = "Me" 62 | description = "My app" 63 | license = "MIT" 64 | 65 | [Deps] 66 | Requires: "nim >= 0.11.0, httpform >= 0.1.0" 67 | ``` 68 | 69 | then you can import **httpform**: 70 | 71 | ``` 72 | import httpform 73 | 74 | // TODO 75 | ``` 76 | 77 | API 78 | ---- 79 | 80 | ``` 81 | FormError = object of Exception 82 | ``` 83 | 84 | raised for invalid content-type or request body. 85 | 86 | ``` 87 | HttpForm = ref object 88 | tmp: string 89 | keepExtname: bool 90 | ``` 91 | 92 | form parser. 93 | 94 | ``` 95 | AsyncHttpForm = HttpForm 96 | ``` 97 | 98 | asynchronous form parser. 99 | 100 | ``` 101 | proc newHttpForm(tmp: string, keepExtname = true): HttpForm 102 | ``` 103 | 104 | creates a new form parser. `tmp` should be set, which will save the uploaded temporary file. If `keepExtname` is true, the extname will be reserved. 105 | 106 | ``` 107 | proc newAsyncHttpForm*(tmp: string, keepExtname = true): AsyncHttpForm 108 | ``` 109 | 110 | creates a new asynchronous form parser. 111 | 112 | ``` 113 | proc parse(x: HttpForm, contentType: string, body: string): 114 | tuple[fields: JsonNode, files: JsonNode] 115 | {.tags: [WriteIOEffect, ReadIOEffect].} 116 | ``` 117 | 118 | parse http request body. 119 | 120 | ``` 121 | proc parseAsync(x: AsyncHttpForm, contentType: string, body: string): 122 | Future[tuple[fields: JsonNode, files: JsonNode]] 123 | {.async, tags: [RootEffect, WriteIOEffect, ReadIOEffect].} 124 | ``` 125 | 126 | asynchronous parse http request body. 127 | -------------------------------------------------------------------------------- /examples/server.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | strutils, os, strtabs, json 3 | 4 | proc main() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | html = """ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | """ 22 | 23 | proc cb(req: Request) {.async.} = 24 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 25 | if req.reqMethod.toLower() == "get": 26 | await req.respond(Http200, html) 27 | else: 28 | echo fields 29 | echo files 30 | await req.respond(Http200, "OK") 31 | waitFor server.serve(Port(8000), cb) 32 | 33 | main() -------------------------------------------------------------------------------- /httpform.nim: -------------------------------------------------------------------------------- 1 | import strutils, os, oids, base64, asyncfile, asyncdispatch, json 2 | 3 | include "./multipart" 4 | 5 | type 6 | HttpForm = ref object 7 | tmp: string 8 | keepExtname: bool 9 | AsyncHttpForm = HttpForm 10 | 11 | template newHttpFormImpl(result: expr) = 12 | new(result) 13 | result.tmp = tmp 14 | result.keepExtname = keepExtname 15 | 16 | proc newHttpForm*(tmp: string, keepExtname = true): HttpForm = 17 | newHttpFormImpl(result) 18 | 19 | proc newAsyncHttpForm*(tmp: string, keepExtname = true): AsyncHttpForm = 20 | newHttpFormImpl(result) 21 | 22 | proc parseUrlencoded(body: string): JsonNode {.inline.} = 23 | var 24 | a: seq[string] 25 | i, h: int 26 | result = newJObject() 27 | for s in body.split('&'): 28 | if s.len() == 0 or s == "=": 29 | result[""] = newJString("") 30 | else: 31 | i = s.find('=') 32 | h = s.high() 33 | if i == -1: 34 | result[s] = newJString("") 35 | elif i == 0: 36 | result[""] = newJString(s[i+1..h]) 37 | elif i == h: 38 | result[s[0..h-1]] = newJString("") 39 | else: 40 | result[s[0..i-1]] = newJString(s[i+1..h]) 41 | 42 | template parseOctetStreamImpl(write: stmt) {.immediate.} = 43 | var options = newJArray() 44 | options.add(newFile(path, body.len(), "application/octet-stream", nil)) 45 | result = newJObject() 46 | result[nil] = options 47 | write 48 | 49 | proc parseOctetStream(body: string, tmp: string): JsonNode {.tags: [WriteIOEffect].} = 50 | var path = joinPath(tmp, $genOid()) 51 | parseOctetStreamImpl: 52 | writeFile(path, body) 53 | 54 | proc parseOctetStreamAsync(body: string, tmp: string): Future[JsonNode] 55 | {.async, tags: [WriteIOEffect, RootEffect].} = 56 | var path = joinPath(tmp, $genOid()) 57 | parseOctetStreamImpl: 58 | await writeFileAsync(path, body) 59 | 60 | proc parseUnknown(body: string): JsonNode = 61 | result = newJObject() 62 | result[nil] = newJString(body) 63 | 64 | proc parse*(x: HttpForm, contentType: string, body: string): 65 | tuple[fields: JsonNode, files: JsonNode] 66 | {.tags: [WriteIOEffect, ReadIOEffect].} = 67 | var 68 | i: int 69 | s: string 70 | if not body.isNil() and body.len() > 0: 71 | if contentType.isNil(): 72 | raise newException(FormError, "bad content-type header, no content-type") 73 | case contentType.toLower() 74 | of "application/json": 75 | result.fields = body.parseJson() 76 | of "application/x-www-form-urlencoded": 77 | result.fields = body.parseUrlencoded() 78 | of "application/octet-stream": 79 | result.files = body.parseOctetStream(x.tmp) 80 | else: 81 | s = contentType.toLower() 82 | i = s.find("multipart/form-data; ") 83 | if i == 0: 84 | i = s.find("boundary=") 85 | if i > -1 and i + "boundary=".len() < s.len(): 86 | result = body.parseMultipart(contentType[i + "boundary=".len()..s.len()-1], 87 | x.tmp, x.keepExtname) 88 | else: 89 | raise newException(FormError, "bad content-type header, no multipart boundary") 90 | else: 91 | result.fields = body.parseUnknown() 92 | 93 | proc parseAsync*(x: AsyncHttpForm, contentType: string, body: string): 94 | Future[tuple[fields: JsonNode, files: JsonNode]] 95 | {.async, tags: [RootEffect, WriteIOEffect, ReadIOEffect].} = 96 | var 97 | i: int 98 | s: string 99 | if not body.isNil() and body.len() > 0: 100 | if contentType.isNil(): 101 | raise newException(FormError, "bad content-type header, no content-type") 102 | case contentType.toLower() 103 | of "application/json": 104 | result.fields = body.parseJson() 105 | of "application/x-www-form-urlencoded": 106 | result.fields = body.parseUrlencoded() 107 | of "application/octet-stream": 108 | result.files = await body.parseOctetStreamAsync(x.tmp) 109 | else: 110 | s = contentType.toLower() 111 | i = s.find("multipart/form-data; ") 112 | if i == 0: 113 | i = s.find("boundary=") 114 | if i > -1 and i + "boundary=".len() < s.len(): 115 | result = await body.parseMultipartAsync(contentType[i + "boundary=".len()..s.len()-1], 116 | x.tmp, x.keepExtname) 117 | else: 118 | raise newException(FormError, "bad content-type header, no multipart boundary") 119 | else: 120 | result.fields = body.parseUnknown() 121 | 122 | when isMainModule: 123 | proc main() = 124 | var 125 | data = 126 | "--AaB03x\r\n" & 127 | "Content-Disposition: form-data; name=\"username\"\r\n\r\n" & 128 | "Tom\r\n" & 129 | 130 | "--AaB03x\r\n" & 131 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file1.txt\"\r\n" & 132 | "Content-Type: text/plain\r\n\r\n" & 133 | "000000\r\n" & 134 | "111111\r\n" & 135 | "010101\r\n\r\n" & 136 | 137 | "--AaB03x\r\n" & 138 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file2.gif\"\r\n" & 139 | "Content-Type: image/gif\r\n" & 140 | "Content-Transfer-Encoding: base64\r\n\r\n" & 141 | "010101010101\r\n" & 142 | 143 | "--AaB03x--\r\n" 144 | 145 | form = newHttpForm(getTempDir()) 146 | 147 | (fields, files) = form.parse("multipart/form-data; boundary=AaB03x", data) 148 | 149 | assert fields["username"] == newJString("Tom") 150 | echo files["upload"][0]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000000.txt" 151 | assert files["upload"][0]["size"] == newJInt(24) 152 | assert files["upload"][0]["type"] == newJString("text/plain") 153 | assert files["upload"][0]["name"] == newJString("file1.txt") 154 | echo files["upload"][1]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000001.gif" 155 | assert files["upload"][1]["size"] == newJInt(12) 156 | assert files["upload"][1]["type"] == newJString("image/gif") 157 | assert files["upload"][1]["name"] == newJString("file2.gif") 158 | 159 | proc mainAsync() {.async.} = 160 | var 161 | data = 162 | "--AaB03x\r\n" & 163 | "Content-Disposition: form-data; name=\"username\"\r\n\r\n" & 164 | "Tom\r\n" & 165 | 166 | "--AaB03x\r\n" & 167 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file1.txt\"\r\n" & 168 | "Content-Type: text/plain\r\n\r\n" & 169 | "000000\r\n" & 170 | "111111\r\n" & 171 | "010101\r\n\r\n" & 172 | 173 | "--AaB03x\r\n" & 174 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file2.gif\"\r\n" & 175 | "Content-Type: image/gif\r\n" & 176 | "Content-Transfer-Encoding: base64\r\n\r\n" & 177 | "010101010101\r\n" & 178 | 179 | "--AaB03x--\r\n" 180 | 181 | form = newAsyncHttpForm(getTempDir()) 182 | 183 | (fields, files) = await form.parseAsync("multipart/form-data; boundary=AaB03x", data) 184 | 185 | assert fields["username"] == newJString("Tom") 186 | echo files["upload"][0]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000000.txt" 187 | assert files["upload"][0]["size"] == newJInt(24) 188 | assert files["upload"][0]["type"] == newJString("text/plain") 189 | assert files["upload"][0]["name"] == newJString("file1.txt") 190 | echo files["upload"][1]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000001.gif" 191 | assert files["upload"][1]["size"] == newJInt(12) 192 | assert files["upload"][1]["type"] == newJString("image/gif") 193 | assert files["upload"][1]["name"] == newJString("file2.gif") 194 | # poll() 195 | # poll() 196 | 197 | asyncCheck mainAsync() 198 | #main() -------------------------------------------------------------------------------- /httpform.nimble: -------------------------------------------------------------------------------- 1 | [Package] 2 | name = "httpform" 3 | version = "0.1.0" 4 | author = "Wang Tong" 5 | description = "Http request form parser" 6 | license = "MIT" 7 | 8 | [Deps] 9 | Requires: "nim >= 0.11.0" 10 | -------------------------------------------------------------------------------- /incl.nim: -------------------------------------------------------------------------------- 1 | type 2 | FormError = object of Exception 3 | 4 | proc newFile(path: string, size: int, ctype: string, filename: string): JsonNode = 5 | result = newJObject() 6 | result["path"] = newJString(path) 7 | result["size"] = newJInt(size) 8 | result["type"] = newJString(ctype) 9 | result["name"] = newJString(filename) 10 | 11 | proc delQuote(s: string): string = 12 | # "a" => a 13 | var length = s.len() 14 | if s == "\"\"": 15 | return "" 16 | if length < 3: 17 | return s 18 | if s[0] == '\"' and s[length-1] == '\"': 19 | return s[1..length-2] 20 | return s -------------------------------------------------------------------------------- /multipart.nim: -------------------------------------------------------------------------------- 1 | import strutils, os, oids, base64, asyncfile, asyncdispatch, json 2 | 3 | include "incl.nim" 4 | 5 | type 6 | MultipartState = enum 7 | msBeginTk, msEndTk, msDisposition, msContent 8 | Multipart = ref object 9 | beginTk, endTk, body: string 10 | length, head, tail: int 11 | state: MultipartState 12 | disposition, name, filename, ctype, encoding: string 13 | tmp:string 14 | keepExtname: bool 15 | 16 | proc newMultipart(body, boundary: string, tmp:string, keepExtname: bool): Multipart = 17 | new(result) 18 | result.head = -1 19 | result.tail = -1 20 | result.state = msBeginTk 21 | result.body = body 22 | result.length = body.len() 23 | result.beginTk = "--" & boundary & "\r\n" 24 | result.endTk = "--" & boundary & "--\r\n" 25 | result.tmp = tmp 26 | result.keepExtname = keepExtname 27 | 28 | proc moveLine(x: Multipart) = 29 | if x.tail < x.length: 30 | x.tail.inc() 31 | x.head = x.tail 32 | while true: 33 | if x.tail == x.length: 34 | break 35 | if x.body[x.tail] == '\13' and 36 | x.tail + 1 < x.length and 37 | x.body[x.tail + 1] == '\10': # \r\n 38 | x.tail.inc() 39 | break 40 | x.tail.inc() 41 | 42 | proc moveParagraph(x: Multipart) = 43 | if x.tail < x.length: 44 | x.tail.inc() 45 | x.head = x.tail 46 | while true: 47 | if x.tail == x.length: 48 | break 49 | if x.body[x.tail] == '\13' and 50 | x.tail + 3 < x.length and 51 | x.body[x.tail] == '\13' and 52 | x.body[x.tail + 1] == '\10' and 53 | x.body[x.tail + 2] == '\13' and 54 | x.body[x.tail + 3] == '\10': # \r\n\r\n 55 | x.tail.inc(3) 56 | break 57 | x.tail.inc() 58 | 59 | proc pickBeginTk(x: Multipart) = 60 | # echo ">>> beginTk" 61 | x.moveLine() 62 | if x.tail == x.length or x.body[x.head..x.tail] != x.beginTk: 63 | raise newException(FormError, "bad multipart boundary") 64 | x.state = msDisposition 65 | 66 | proc pickDisposition(x: Multipart) = 67 | # echo ">>> disosition" 68 | x.moveParagraph() 69 | if x.tail == x.length or x.tail - 4 < x.head: 70 | raise newException(FormError, "bad multipart disposition") 71 | 72 | var 73 | dispItems: seq[string] 74 | i, h: int 75 | 76 | x.disposition = nil 77 | x.name = nil 78 | x.filename = nil 79 | x.ctype = nil 80 | x.encoding = nil 81 | 82 | for line in x.body[x.head..x.tail-4].split("\r\n"): 83 | i = line.find(": ") 84 | h = line.high() 85 | if i > 0 and i < h-1: 86 | case line[0..i-1].toLower() 87 | of "content-disposition": 88 | dispItems = line[i+2..h].split("; ") 89 | if dispItems[0] == "form-data": # disp? 90 | x.disposition = dispItems[0] 91 | for s in dispItems: 92 | i = s.find('=') 93 | h = s.high() 94 | if i > 0: 95 | case s[0..i-1].toLower() 96 | of "name": 97 | x.name = if i < h: s[i+1..h].delQuote() else: "" 98 | of "filename": 99 | x.filename = if i < h: s[i+1..h].delQuote() else: "" 100 | else: 101 | discard 102 | of "content-type": 103 | x.ctype = line[i+2..h] 104 | of "content-transfer-Encoding": 105 | x.encoding = line[i+2..h] 106 | else: 107 | discard 108 | 109 | if x.disposition != "form-data": 110 | raise newException(FormError, "bad multipart disposition") 111 | 112 | # echo x.disposition 113 | # echo x.ctype 114 | # echo x.name 115 | # echo x.filename 116 | 117 | x.state = msContent 118 | 119 | template doWrite(content: string) = 120 | var 121 | path: string 122 | if not x.name.isNil(): 123 | # echo "---", repr content 124 | # echo "---" 125 | if x.filename.isNil(): 126 | result.fields[x.name] = newJString(content) 127 | else: 128 | path = joinPath(x.tmp, if x.keepExtname: $genOid() & splitFile(x.filename).ext 129 | else: $genOid()) 130 | case x.encoding 131 | of nil, "binary", "7bit", "8bit": writeFile(path, content) 132 | of "base64": writeFile(path, decode(content)) 133 | else: raise newException(FormError, "unknow transfer encoding") 134 | if not result.files.hasKey(x.name): result.files[x.name] = newJArray() 135 | result.files[x.name].add(newFile(path, content.len(), x.ctype, x.filename)) 136 | 137 | template pickContent(x: Multipart) = 138 | # echo ">>> content" 139 | var 140 | begin = x.tail + 1 141 | finish: int 142 | text: string 143 | while true: 144 | x.moveLine() 145 | if x.tail == x.length: 146 | raise newException(FormError, "bad multipart content") 147 | text = x.body[x.head..x.tail] 148 | if text == x.beginTk: 149 | finish = x.tail - x.beginTk.len() - 2 150 | if finish >= begin: 151 | doWrite(x.body[begin..finish]) 152 | x.state = msDisposition 153 | break 154 | if text == x.endTk: 155 | finish = x.tail - x.endTk.len() - 2 156 | if finish >= begin: 157 | doWrite(x.body[begin..finish]) 158 | x.state = msEndTk 159 | break 160 | 161 | proc parseMultipart(body: string, boundary: string, tmp: string, keepExtname: bool): 162 | tuple[fields: JsonNode, files: JsonNode] 163 | {.tags: [WriteIOEffect].} = 164 | var x = newMultipart(body, boundary, tmp, keepExtname) 165 | result.fields = newJObject() 166 | result.files = newJObject() 167 | while true: 168 | case x.state 169 | of msBeginTk: x.pickBeginTk() 170 | of msDisposition: x.pickDisposition() 171 | of msContent: x.pickContent() 172 | of msEndTk: break 173 | else: discard 174 | 175 | proc writeFileAsync(path: string, content: string) {.async.} = 176 | var file = openAsync(path, fmWrite) 177 | await file.write(content) 178 | file.close() 179 | 180 | proc doWriteAsync(x: Multipart, content: string, 181 | r: tuple[fields: JsonNode, files: JsonNode]) {.async.} = 182 | var 183 | path: string 184 | if not x.name.isNil(): 185 | # echo "---", repr content 186 | # echo "---" 187 | if x.filename.isNil(): 188 | r.fields[x.name] = newJString(content) 189 | else: 190 | path = joinPath(x.tmp, if x.keepExtname: $genOid() & splitFile(x.filename).ext 191 | else: $genOid()) 192 | case x.encoding 193 | of nil, "binary", "7bit", "8bit": await writeFileAsync(path, content) 194 | of "base64": await writeFileAsync(path, decode(content)) 195 | else: raise newException(FormError, "unknow transfer encoding") 196 | if not r.files.hasKey(x.name): r.files[x.name] = newJArray() 197 | r.files[x.name].add(newFile(path, content.len(), x.ctype, x.filename)) 198 | 199 | proc pickContentAsync(x: Multipart, r: tuple[fields: JsonNode, files: JsonNode]) {.async.} = 200 | # echo ">>> content" 201 | var 202 | begin = x.tail + 1 203 | finish: int 204 | text: string 205 | while true: 206 | x.moveLine() 207 | if x.tail == x.length: 208 | raise newException(FormError, "bad multipart content") 209 | text = x.body[x.head..x.tail] 210 | if text == x.beginTk: 211 | finish = x.tail - x.beginTk.len() - 2 212 | if finish >= begin: 213 | await x.doWriteAsync(x.body[begin..finish], r) 214 | x.state = msDisposition 215 | break 216 | if text == x.endTk: 217 | finish = x.tail - x.endTk.len() - 2 218 | if finish >= begin: 219 | await x.doWriteAsync(x.body[begin..finish], r) 220 | x.state = msEndTk 221 | break 222 | 223 | proc parseMultipartAsync(body: string, boundary: string, tmp: string, keepExtname: bool): 224 | Future[tuple[fields: JsonNode, files: JsonNode]] 225 | {.async, tags: [RootEffect, WriteIOEffect, ReadIOEffect].} = 226 | var x = newMultipart(body, boundary, tmp, keepExtname) 227 | result.fields = newJObject() 228 | result.files = newJObject() 229 | while true: 230 | case x.state 231 | of msBeginTk: x.pickBeginTk() 232 | of msDisposition: x.pickDisposition() 233 | of msContent: await x.pickContentAsync(result) 234 | of msEndTk: break 235 | else: discard 236 | -------------------------------------------------------------------------------- /tests/appjson.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | threadpool, net, os, strtabs, json 3 | 4 | proc server() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | proc cb(req: Request) {.async.} = 9 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 10 | assert fields["b"][0]["a"] == newJInt(2) 11 | assert files == nil 12 | quit(0) 13 | waitFor server.serve(Port(8000), cb) 14 | 15 | proc client() = 16 | var 17 | socket = newSocket() 18 | data = """{"a":1, "b":[{"a":2}, {"c":3}], "c":null}""" 19 | socket.connect("127.0.0.1", Port(8000)) 20 | socket.send("POST /path HTTP/1.1\r\L") 21 | socket.send("Content-Type: application/json\r\L") 22 | socket.send("Content-Length: " & $data.len() & "\r\L\r\L") 23 | socket.send(data) 24 | 25 | proc main() = 26 | parallel: 27 | spawn server() 28 | sleep(100) 29 | spawn client() 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /tests/multipart.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | threadpool, net, os, strtabs, json 3 | 4 | proc server() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | proc cb(req: Request) {.async.} = 9 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 10 | assert fields["username"] == newJString("Tom") 11 | echo files["upload"][0]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000000.txt" 12 | assert files["upload"][0]["size"] == newJInt(6) 13 | assert files["upload"][0]["type"] == newJString("text/plain") 14 | assert files["upload"][0]["name"] == newJString("file1.txt") 15 | echo files["upload"][1]["path"] # "/home/king/tmp/55cdf98a0fbeb30400000001.gif" 16 | assert files["upload"][1]["size"] == newJInt(12) 17 | assert files["upload"][1]["type"] == newJString("image/gif") 18 | assert files["upload"][1]["name"] == newJString("file2.gif") 19 | quit(0) 20 | waitFor server.serve(Port(8000), cb) 21 | 22 | proc client() = 23 | var 24 | socket = newSocket() 25 | data = "--AaB03x\r\n" & 26 | "Content-Disposition: form-data; name=\"username\"\r\n\r\n" & 27 | "Tom\r\n" & 28 | 29 | "--AaB03x\r\n" & 30 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file1.txt\"\r\n" & 31 | "Content-Type: text/plain\r\n\r\n" & 32 | "000000\r\n" & 33 | 34 | "--AaB03x\r\n" & 35 | "Content-Disposition: form-data; name=\"upload\"; filename=\"file2.gif\"\r\n" & 36 | "Content-Type: image/gif\r\n" & 37 | "Content-Transfer-Encoding: base64\r\n\r\n" & 38 | "010101010101\r\n" & 39 | 40 | "--AaB03x--\r\n" 41 | 42 | socket.connect("127.0.0.1", Port(8000)) 43 | socket.send("POST /path HTTP/1.1\r\L") 44 | socket.send("Content-Type: multipart/form-data; boundary=AaB03x\r\L") 45 | socket.send("Content-Length: " & $data.len() & "\r\L\r\L") 46 | socket.send(data) 47 | 48 | proc main() = 49 | parallel: 50 | spawn server() 51 | sleep(100) 52 | spawn client() 53 | 54 | main() 55 | -------------------------------------------------------------------------------- /tests/octetstream.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | threadpool, net, os, strtabs, json 3 | 4 | proc server() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | proc cb(req: Request) {.async.} = 9 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 10 | assert fields == nil 11 | echo files[nil][0]["path"] 12 | assert files[nil][0]["size"] == newJInt(6) 13 | assert files[nil][0]["name"] == newJString(nil) 14 | assert files[nil][0]["type"] == newJString("application/octet-stream") 15 | quit(0) 16 | waitFor server.serve(Port(8000), cb) 17 | 18 | proc client() = 19 | var 20 | socket = newSocket() 21 | data = "000000" 22 | socket.connect("127.0.0.1", Port(8000)) 23 | socket.send("POST /path HTTP/1.1\r\n") 24 | socket.send("Content-Type: application/octet-stream\r\n") 25 | socket.send("Content-Length: " & $data.len() & "\r\n\r\n") 26 | socket.send(data) 27 | 28 | proc main() = 29 | parallel: 30 | spawn server() 31 | sleep(100) 32 | spawn client() 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /tests/unknown.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | threadpool, net, os, strtabs, json 3 | 4 | proc server() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | proc cb(req: Request) {.async.} = 9 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 10 | assert fields[nil] == newJString("Hello world!") 11 | assert files == nil 12 | quit(0) 13 | waitFor server.serve(Port(8000), cb) 14 | 15 | proc client() = 16 | var 17 | socket = newSocket() 18 | socket.connect("127.0.0.1", Port(8000)) 19 | socket.send("POST /path HTTP/1.1\r\L") 20 | socket.send("Content-Type: text/plain\r\L") 21 | socket.send("Content-Length: 12\r\L\r\L") 22 | socket.send("Hello world!") 23 | 24 | proc main() = 25 | parallel: 26 | spawn server() 27 | sleep(100) 28 | spawn client() 29 | 30 | main() 31 | -------------------------------------------------------------------------------- /tests/urlencoded.nim: -------------------------------------------------------------------------------- 1 | import ../httpform, asyncdispatch, asynchttpserver, 2 | threadpool, net, os, strtabs, json 3 | 4 | proc server() = 5 | var 6 | server = newAsyncHttpServer() 7 | form = newAsyncHttpForm(getTempDir(), true) 8 | proc cb(req: Request) {.async.} = 9 | var (fields, files) = await form.parseAsync(req.headers["Content-Type"], req.body) 10 | assert fields["a"] == newJString("100") 11 | assert files == nil 12 | quit(0) 13 | waitFor server.serve(Port(8000), cb) 14 | 15 | proc client() = 16 | var 17 | socket = newSocket() 18 | data = """a=100&b=200&&x&&m&&""" 19 | socket.connect("127.0.0.1", Port(8000)) 20 | socket.send("POST /path HTTP/1.1\r\L") 21 | socket.send("Content-Type: application/x-www-form-urlencoded\r\L") 22 | socket.send("Content-Length: " & $data.len() & "\r\L\r\L") 23 | socket.send(data) 24 | 25 | proc main() = 26 | parallel: 27 | spawn server() 28 | sleep(100) 29 | spawn client() 30 | 31 | main() 32 | --------------------------------------------------------------------------------