| 1 | // vtest build: !windows |
| 2 | import net.http |
| 3 | import net.urllib |
| 4 | import net |
| 5 | import io |
| 6 | import time |
| 7 | |
| 8 | struct StringReader { |
| 9 | text string |
| 10 | mut: |
| 11 | place int |
| 12 | } |
| 13 | |
| 14 | fn (mut s StringReader) read(mut buf []u8) !int { |
| 15 | if s.place >= s.text.len { |
| 16 | return io.Eof{} |
| 17 | } |
| 18 | max_bytes := 100 |
| 19 | end := if s.place + max_bytes >= s.text.len { s.text.len } else { s.place + max_bytes } |
| 20 | n := copy(mut buf, s.text[s.place..end].bytes()) |
| 21 | s.place += n |
| 22 | return n |
| 23 | } |
| 24 | |
| 25 | fn reader(s string) &io.BufferedReader { |
| 26 | return io.new_buffered_reader( |
| 27 | reader: &StringReader{ |
| 28 | text: s |
| 29 | } |
| 30 | ) |
| 31 | } |
| 32 | |
| 33 | fn test_parse_request_not_http() { |
| 34 | mut reader__ := reader('hello') |
| 35 | http.parse_request(mut reader__) or { return } |
| 36 | panic('should not have parsed') |
| 37 | } |
| 38 | |
| 39 | fn test_parse_request_no_headers() { |
| 40 | mut reader_ := reader('GET / HTTP/1.1\r\n\r\n') |
| 41 | req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } |
| 42 | assert req.method == .get |
| 43 | assert req.url == '/' |
| 44 | assert req.version == .v1_1 |
| 45 | } |
| 46 | |
| 47 | fn test_parse_request_two_headers() { |
| 48 | mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: B\r\n\r\n') |
| 49 | req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } |
| 50 | assert req.header.custom_values('Test1') == ['a'] |
| 51 | assert req.header.custom_values('Test2') == ['B'] |
| 52 | } |
| 53 | |
| 54 | fn test_parse_request_two_header_values() { |
| 55 | mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n') |
| 56 | req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } |
| 57 | assert req.header.custom_values('Test1') == ['a; b'] |
| 58 | assert req.header.custom_values('Test2') == ['c', 'd'] |
| 59 | } |
| 60 | |
| 61 | fn test_parse_request_body() { |
| 62 | mut reader_ := |
| 63 | reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: b\r\nContent-Length: 4\r\n\r\nbodyabc') |
| 64 | req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } |
| 65 | assert req.data == 'body' |
| 66 | } |
| 67 | |
| 68 | fn test_parse_request_line() { |
| 69 | method, target, version := http.parse_request_line('GET /target HTTP/1.1') or { |
| 70 | panic('did not parse: ${err}') |
| 71 | } |
| 72 | assert method == .get |
| 73 | assert target.str() == '/target' |
| 74 | assert version == .v1_1 |
| 75 | } |
| 76 | |
| 77 | fn test_parse_request_uri_with_consecutive_slashes() { |
| 78 | url := urllib.parse_request_uri('//another.html') or { panic('did not parse: ${err}') } |
| 79 | assert url.host == '' |
| 80 | assert url.path == '//another.html' |
| 81 | assert url.str() == '//another.html' |
| 82 | |
| 83 | absolute := urllib.parse_request_uri('http://localhost:8080//another.html') or { |
| 84 | panic('did not parse: ${err}') |
| 85 | } |
| 86 | assert absolute.host == 'localhost:8080' |
| 87 | assert absolute.path == '//another.html' |
| 88 | assert absolute.str() == 'http://localhost:8080//another.html' |
| 89 | } |
| 90 | |
| 91 | fn test_parse_request_line_with_consecutive_slashes() { |
| 92 | method, target, version := http.parse_request_line('GET //another.html HTTP/1.1') or { |
| 93 | panic('did not parse: ${err}') |
| 94 | } |
| 95 | assert method == .get |
| 96 | assert target.host == '' |
| 97 | assert target.path == '//another.html' |
| 98 | assert target.str() == '//another.html' |
| 99 | assert version == .v1_1 |
| 100 | } |
| 101 | |
| 102 | fn test_parse_form() { |
| 103 | assert http.parse_form('foo=bar&bar=baz') == { |
| 104 | 'foo': 'bar' |
| 105 | 'bar': 'baz' |
| 106 | } |
| 107 | assert http.parse_form('foo=bar=&bar=baz') == { |
| 108 | 'foo': 'bar=' |
| 109 | 'bar': 'baz' |
| 110 | } |
| 111 | assert http.parse_form('foo=bar%3D&bar=baz') == { |
| 112 | 'foo': 'bar=' |
| 113 | 'bar': 'baz' |
| 114 | } |
| 115 | assert http.parse_form('foo=b%26ar&bar=baz') == { |
| 116 | 'foo': 'b&ar' |
| 117 | 'bar': 'baz' |
| 118 | } |
| 119 | assert http.parse_form('a=b& c=d') == { |
| 120 | 'a': 'b' |
| 121 | ' c': 'd' |
| 122 | } |
| 123 | assert http.parse_form('a=b&c= d ') == { |
| 124 | 'a': 'b' |
| 125 | 'c': ' d ' |
| 126 | } |
| 127 | assert http.parse_form('{json}') == { |
| 128 | 'json': '{json}' |
| 129 | } |
| 130 | assert http.parse_form('{ |
| 131 | "_id": "76c", |
| 132 | "friends": [ |
| 133 | { |
| 134 | "id": 0, |
| 135 | "name": "Mason Luna" |
| 136 | } |
| 137 | ], |
| 138 | "greeting": "Hello." |
| 139 | }') == { |
| 140 | 'json': '{ |
| 141 | "_id": "76c", |
| 142 | "friends": [ |
| 143 | { |
| 144 | "id": 0, |
| 145 | "name": "Mason Luna" |
| 146 | } |
| 147 | ], |
| 148 | "greeting": "Hello." |
| 149 | }' |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | fn test_parse_multipart_form() { |
| 154 | boundary := '6844a625b1f0b299' |
| 155 | names := ['foo', 'fooz'] |
| 156 | file := 'bar.v' |
| 157 | ct := 'application/octet-stream' |
| 158 | contents := ['baz', 'buzz'] |
| 159 | data := "--${boundary} |
| 160 | Content-Disposition: form-data; name=\"${names[0]}\"; filename=\"${file}\"\r |
| 161 | Content-Type: ${ct}\r |
| 162 | \r |
| 163 | ${contents[0]}\r |
| 164 | --${boundary}\r |
| 165 | Content-Disposition: form-data; name=\"${names[1]}\"\r |
| 166 | \r |
| 167 | ${contents[1]}\r |
| 168 | --${boundary}--\r |
| 169 | " |
| 170 | form, files := http.parse_multipart_form(data, boundary) |
| 171 | assert files == { |
| 172 | names[0]: [ |
| 173 | http.FileData{ |
| 174 | filename: file |
| 175 | content_type: ct |
| 176 | data: contents[0] |
| 177 | }, |
| 178 | ] |
| 179 | } |
| 180 | |
| 181 | assert form == { |
| 182 | names[1]: contents[1] |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | fn test_parse_multipart_form2() { |
| 187 | boundary := '---------------------------27472781931927549291906391339' |
| 188 | data := '--${boundary}\r |
| 189 | Content-Disposition: form-data; name="username"\r |
| 190 | \r |
| 191 | admin\r |
| 192 | --${boundary}\r |
| 193 | Content-Disposition: form-data; name="password"\r |
| 194 | \r |
| 195 | admin123\r |
| 196 | --${boundary}--\r |
| 197 | ' |
| 198 | form, files := http.parse_multipart_form(data, boundary) |
| 199 | for k, v in form { |
| 200 | eprintln('> k: ${k} | v: ${v}') |
| 201 | eprintln('>> k.bytes(): ${k.bytes()}') |
| 202 | eprintln('>> v.bytes(): ${v.bytes()}') |
| 203 | } |
| 204 | assert form['username'] == 'admin' |
| 205 | assert form['password'] == 'admin123' |
| 206 | } |
| 207 | |
| 208 | fn test_multipart_form_body() { |
| 209 | files := { |
| 210 | 'foo': [ |
| 211 | http.FileData{ |
| 212 | filename: 'bar.v' |
| 213 | content_type: 'application/octet-stream' |
| 214 | data: 'baz' |
| 215 | }, |
| 216 | ] |
| 217 | } |
| 218 | form := { |
| 219 | 'fooz': 'buzz' |
| 220 | } |
| 221 | |
| 222 | body, boundary := http.multipart_form_body(form, files) |
| 223 | parsed_form, parsed_files := http.parse_multipart_form(body, boundary) |
| 224 | assert parsed_files == files |
| 225 | assert parsed_form == form |
| 226 | } |
| 227 | |
| 228 | fn test_parse_large_body() { |
| 229 | body := 'A'.repeat(10_001) // greater than max_bytes |
| 230 | req := 'GET / HTTP/1.1\r\nContent-Length: ${body.len}\r\n\r\n${body}' |
| 231 | mut reader_ := reader(req) |
| 232 | result := http.parse_request(mut reader_)! |
| 233 | assert result.data.len == body.len |
| 234 | assert result.data == body |
| 235 | } |
| 236 | |
| 237 | fn test_parse_multipart_form_empty_body() { |
| 238 | body := '' |
| 239 | boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b' |
| 240 | form, files := http.parse_multipart_form(body, boundary) |
| 241 | assert form.len == 0 |
| 242 | assert files.len == 0 |
| 243 | } |
| 244 | |
| 245 | fn test_parse_multipart_form_issue_26204__do_not_panic_for_small_or_partial_forms() { |
| 246 | boundary := '----01KDN6J6BKWY9WMYWRW4MG5J59' |
| 247 | body := '${boundary}\r\nContent-Disposition: form-data; name="fooz"${boundary}--\r\n' |
| 248 | form, files := http.parse_multipart_form(body, boundary) |
| 249 | assert form.len == 0 |
| 250 | assert files.len == 0 |
| 251 | } |
| 252 | |
| 253 | fn test_parse_multipart_form_issue_24974_raw() { |
| 254 | body := r'------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n' |
| 255 | boundary := r'----WebKitFormBoundaryQcBIkwnOACVsvR8b' |
| 256 | form, files := http.parse_multipart_form(body, boundary) |
| 257 | assert form.len == 0 |
| 258 | assert files.len == 0 |
| 259 | } |
| 260 | |
| 261 | fn test_parse_multipart_form_issue_24974_cooked() { |
| 262 | body := '------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n' |
| 263 | boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b' |
| 264 | form, files := http.parse_multipart_form(body, boundary) |
| 265 | assert form.len == 0 |
| 266 | assert files.len == 1 |
| 267 | assert files['files'][0].filename == 'mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg' |
| 268 | assert files['files'][0].content_type == 'image/jpeg' |
| 269 | } |
| 270 | |
| 271 | fn test_parse_request_head_str_basic() { |
| 272 | s := 'GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' |
| 273 | req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } |
| 274 | assert req.method == .get |
| 275 | assert req.url == '/' |
| 276 | assert req.version == .v1_1 |
| 277 | assert req.host == 'example.com' |
| 278 | } |
| 279 | |
| 280 | fn test_parse_request_head_str_post_with_headers() { |
| 281 | s := 'POST /api HTTP/1.1\r\nHost: test.com\r\nContent-Type: application/json\r\nContent-Length: 10\r\n\r\n' |
| 282 | req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } |
| 283 | assert req.method == .post |
| 284 | assert req.url == '/api' |
| 285 | assert req.version == .v1_1 |
| 286 | assert req.host == 'test.com' |
| 287 | assert req.header.custom_values('Content-Type') == ['application/json'] |
| 288 | } |
| 289 | |
| 290 | fn test_parse_request_head_str_post_with_headers_and_body() { |
| 291 | s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}' |
| 292 | req := http.parse_request_head_str(s) or { |
| 293 | assert false, 'did not parse: ${err}' |
| 294 | return |
| 295 | } |
| 296 | assert req.method == .post |
| 297 | assert req.url == '/index' |
| 298 | assert req.version == .v1_1 |
| 299 | assert req.host == 'localhost:9008' |
| 300 | assert req.header.custom_values('User-Agent') == ['curl/7.68.0'] |
| 301 | assert req.header.custom_values('Accept') == ['*/*'] |
| 302 | assert req.header.custom_values('Content-Type') == ['application/json'] |
| 303 | assert req.header.custom_values('Connection') == ['keep-alive'] |
| 304 | assert req.data == '' |
| 305 | } |
| 306 | |
| 307 | fn test_parse_request_head_post_with_headers_and_body() { |
| 308 | s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}' |
| 309 | req := http.parse_request_str(s) or { |
| 310 | assert false, 'did not parse: ${err}' |
| 311 | return |
| 312 | } |
| 313 | assert req.data == '{"username": "test"}' |
| 314 | } |
| 315 | |
| 316 | fn test_parse_request_head_str_with_spaces_in_header_values() { |
| 317 | s := 'GET /path HTTP/1.1\r\nX-Custom-Header: value with spaces\r\n\r\n' |
| 318 | req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } |
| 319 | assert req.method == .get |
| 320 | assert req.url == '/path' |
| 321 | assert req.header.custom_values('X-Custom-Header') == ['value with spaces'] |
| 322 | } |
| 323 | |
| 324 | fn test_parse_request_head_str_multiple_same_header() { |
| 325 | s := 'GET / HTTP/1.1\r\nHost: example.com\r\nSet-Cookie: session=abc\r\nSet-Cookie: user=xyz\r\n\r\n' |
| 326 | req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } |
| 327 | assert req.method == .get |
| 328 | assert req.host == 'example.com' |
| 329 | assert req.header.custom_values('Set-Cookie') == ['session=abc', 'user=xyz'] |
| 330 | } |
| 331 | |
| 332 | fn test_get_does_not_wait_for_timeout_when_content_length_is_complete() { |
| 333 | mut listener := net.listen_tcp(.ip, '127.0.0.1:0')! |
| 334 | port := listener.addr()!.port()! |
| 335 | t := spawn fn (mut listener net.TcpListener) { |
| 336 | mut conn := listener.accept() or { |
| 337 | listener.close() or {} |
| 338 | return |
| 339 | } |
| 340 | defer { |
| 341 | conn.close() or {} |
| 342 | listener.close() or {} |
| 343 | } |
| 344 | |
| 345 | mut request_buf := []u8{len: 2048} |
| 346 | _ = conn.read(mut request_buf) or { return } |
| 347 | response := 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok' |
| 348 | conn.write(response.bytes()) or { return } |
| 349 | |
| 350 | conn.set_read_timeout(5 * time.second) |
| 351 | mut drain_buf := []u8{len: 128} |
| 352 | for { |
| 353 | n := conn.read(mut drain_buf) or { break } |
| 354 | if n <= 0 { |
| 355 | break |
| 356 | } |
| 357 | } |
| 358 | }(mut listener) |
| 359 | |
| 360 | mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '') |
| 361 | req.read_timeout = 2 * time.second |
| 362 | start := time.now() |
| 363 | res := req.do()! |
| 364 | elapsed := time.since(start) |
| 365 | t.wait() |
| 366 | |
| 367 | assert res.status() == .ok |
| 368 | assert res.body == 'ok' |
| 369 | assert elapsed < time.second |
| 370 | } |
| 371 | |
| 372 | fn test_prepare_uses_fetch_config_timeouts() { |
| 373 | req := http.prepare( |
| 374 | url: 'http://example.com' |
| 375 | read_timeout: 123 * time.millisecond |
| 376 | write_timeout: 456 * time.millisecond |
| 377 | )! |
| 378 | assert req.read_timeout == 123 * time.millisecond |
| 379 | assert req.write_timeout == 456 * time.millisecond |
| 380 | } |
| 381 | |
| 382 | fn test_get_does_not_wait_for_timeout_when_chunked_body_is_complete() { |
| 383 | mut listener := net.listen_tcp(.ip, '127.0.0.1:0')! |
| 384 | port := listener.addr()!.port()! |
| 385 | t := spawn fn (mut listener net.TcpListener) { |
| 386 | mut conn := listener.accept() or { |
| 387 | listener.close() or {} |
| 388 | return |
| 389 | } |
| 390 | defer { |
| 391 | conn.close() or {} |
| 392 | listener.close() or {} |
| 393 | } |
| 394 | |
| 395 | mut request_buf := []u8{len: 2048} |
| 396 | _ = conn.read(mut request_buf) or { return } |
| 397 | response := 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n2\r\nok\r\n0\r\n\r\n' |
| 398 | conn.write(response.bytes()) or { return } |
| 399 | |
| 400 | conn.set_read_timeout(5 * time.second) |
| 401 | mut drain_buf := []u8{len: 128} |
| 402 | for { |
| 403 | n := conn.read(mut drain_buf) or { break } |
| 404 | if n <= 0 { |
| 405 | break |
| 406 | } |
| 407 | } |
| 408 | }(mut listener) |
| 409 | |
| 410 | mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '') |
| 411 | req.read_timeout = 2 * time.second |
| 412 | start := time.now() |
| 413 | res := req.do()! |
| 414 | elapsed := time.since(start) |
| 415 | t.wait() |
| 416 | |
| 417 | assert res.status() == .ok |
| 418 | assert res.body == 'ok' |
| 419 | assert elapsed < time.second |
| 420 | } |
| 421 | |