v2 / vlib / net / http / request.v
1167 lines · 1097 sloc · 32.66 KB · 45545c2fda3dfafa31fb7341b31b786ad143e67d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import io
7import net
8import net.urllib
9import rand
10import strings
11import time
12
13pub type RequestRedirectFn = fn (request &Request, nredirects int, new_url string) !
14
15pub type RequestProgressFn = fn (request &Request, chunk []u8, read_so_far u64) !
16
17pub type RequestProgressBodyFn = fn (request &Request, chunk []u8, body_read_so_far u64, body_expected_size u64, status_code int) !
18
19pub type RequestFinishFn = fn (request &Request, final_size u64) !
20
21// Request holds information about an HTTP request (either received by
22// a server or to be sent by a client)
23pub struct Request {
24mut:
25 cookies map[string]string
26pub mut:
27 version Version = .v1_1
28 method Method = .get
29 header Header
30 host string
31 data string
32 url string
33 user_agent string = 'v.http'
34 verbose bool
35 user_ptr voidptr
36 proxy &HttpProxy = unsafe { nil }
37 // NOT implemented for ssl connections
38 // time = -1 for no timeout
39 read_timeout i64 = 30 * time.second
40 write_timeout i64 = 30 * time.second
41
42 validate bool // when true, certificate failures will stop further processing
43 verify string
44 cert string
45 cert_key string
46 in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file
47 allow_redirect bool = true // whether to allow redirect
48 max_retries int = 5 // maximum number of retries required when an underlying socket error occurs
49 // callbacks to allow custom reporting code to run, while the request is running, and to implement streaming
50 on_redirect RequestRedirectFn = unsafe { nil }
51 on_progress RequestProgressFn = unsafe { nil }
52 on_progress_body RequestProgressBodyFn = unsafe { nil }
53 on_finish RequestFinishFn = unsafe { nil }
54
55 stop_copying_limit i64 = -1 // after this many bytes are received, stop copying to the response. Note that on_progress and on_progress_body callbacks, will continue to fire normally, until the full response is read, which allows you to implement streaming downloads, without keeping the whole big response in memory
56 stop_receiving_limit i64 = -1 // after this many bytes are received, break out of the loop that reads the response, effectively stopping the request early. No more on_progress callbacks will be fired. The on_finish callback will fire.
57}
58
59@[manualfree]
60fn (mut req Request) free() {
61 mut freed_ptrs := map[u64]bool{}
62 unsafe {
63 req.cookies.free()
64 for i := 0; i < req.header.cur_pos; i++ {
65 mut key := req.header.data[i].key
66 mut value := req.header.data[i].value
67 key_ptr := u64(usize(key.str))
68 if key_ptr !in freed_ptrs {
69 key.free()
70 freed_ptrs[key_ptr] = true
71 }
72 value_ptr := u64(usize(value.str))
73 if value_ptr !in freed_ptrs {
74 value.free()
75 freed_ptrs[value_ptr] = true
76 }
77 }
78 mut host := req.host
79 host_ptr := u64(usize(host.str))
80 if host_ptr !in freed_ptrs {
81 host.free()
82 freed_ptrs[host_ptr] = true
83 }
84 mut data := req.data
85 data_ptr := u64(usize(data.str))
86 if data_ptr !in freed_ptrs {
87 data.free()
88 freed_ptrs[data_ptr] = true
89 }
90 mut url := req.url
91 url_ptr := u64(usize(url.str))
92 if url_ptr !in freed_ptrs {
93 url.free()
94 freed_ptrs[url_ptr] = true
95 }
96 mut user_agent := req.user_agent
97 user_agent_ptr := u64(usize(user_agent.str))
98 if user_agent_ptr !in freed_ptrs {
99 user_agent.free()
100 freed_ptrs[user_agent_ptr] = true
101 }
102 mut verify := req.verify
103 verify_ptr := u64(usize(verify.str))
104 if verify_ptr !in freed_ptrs {
105 verify.free()
106 freed_ptrs[verify_ptr] = true
107 }
108 mut cert := req.cert
109 cert_ptr := u64(usize(cert.str))
110 if cert_ptr !in freed_ptrs {
111 cert.free()
112 freed_ptrs[cert_ptr] = true
113 }
114 mut cert_key := req.cert_key
115 cert_key_ptr := u64(usize(cert_key.str))
116 if cert_key_ptr !in freed_ptrs {
117 cert_key.free()
118 freed_ptrs[cert_key_ptr] = true
119 }
120 }
121}
122
123// reset frees request-owned data and resets the request to default values.
124@[manualfree]
125pub fn (mut req Request) reset() {
126 req.free()
127 req = Request{}
128}
129
130// add_header adds the key and value of an HTTP request header
131// To add a custom header, use add_custom_header
132pub fn (mut req Request) add_header(key CommonHeader, val string) {
133 req.header.add(key, val)
134}
135
136// add_custom_header adds the key and value of an HTTP request header
137// This method may fail if the key contains characters that are not permitted
138pub fn (mut req Request) add_custom_header(key string, val string) ! {
139 return req.header.add_custom(key, val)
140}
141
142// add_cookie adds a cookie to the request.
143pub fn (mut req Request) add_cookie(c Cookie) {
144 req.cookies[c.name] = c.value
145}
146
147// cookie returns the named cookie provided in the request or `none` if not found.
148// If multiple cookies match the given name, only one cookie will be returned.
149pub fn (req &Request) cookie(name string) ?Cookie {
150 // TODO(alex) this should work once Cookie is used
151 // return req.cookies[name] or { none }
152
153 if value := req.cookies[name] {
154 return Cookie{
155 name: name
156 value: value
157 }
158 }
159 return none
160}
161
162// do will send the HTTP request and returns `http.Response` as soon as the response is received
163pub fn (req &Request) do() !Response {
164 mut url := urllib.parse(req.url) or { return error('http.Request.do: invalid url ${req.url}') }
165 mut rurl := url
166 mut resp := Response{}
167 mut method := req.method
168 mut data := req.data
169 mut header := req.header
170 mut nredirects := 0
171 for {
172 if nredirects == max_redirects {
173 return error('http.request.do: maximum number of redirects reached (${max_redirects})')
174 }
175 qresp := req.method_and_url_to_response(method, rurl, data, header)!
176 resp = qresp
177 if !req.allow_redirect {
178 break
179 }
180 status := resp.status()
181 if status !in [.moved_permanently, .found, .see_other, .temporary_redirect,
182 .permanent_redirect] {
183 break
184 }
185 // follow any redirects
186 mut redirect_url := resp.header.get(.location) or { '' }
187 if redirect_url.len > 0 && redirect_url[0] == `/` {
188 url.set_path(redirect_url) or {
189 return error('http.request.do: invalid path in redirect: "${redirect_url}"')
190 }
191 redirect_url = url.str()
192 }
193 if req.on_redirect != unsafe { nil } {
194 req.on_redirect(req, nredirects, redirect_url)!
195 }
196 qrurl := urllib.parse(redirect_url) or {
197 return error('http.request.do: invalid URL in redirect "${redirect_url}"')
198 }
199 method, data, header = redirected_request_parts(method, status, data, header)
200 rurl = qrurl
201 nredirects++
202 }
203 return resp
204}
205
206fn redirected_request_parts(method Method, status Status, data string, header Header) (Method, string, Header) {
207 next_method := redirected_method(method, status)
208 if next_method == method {
209 return method, data, header
210 }
211 mut next_header := header
212 next_header.delete(.content_length)
213 next_header.delete(.content_type)
214 next_header.delete(.transfer_encoding)
215 return next_method, '', next_header
216}
217
218fn redirected_method(method Method, status Status) Method {
219 return match status {
220 .see_other {
221 if method == Method.head {
222 Method.head
223 } else {
224 Method.get
225 }
226 }
227 .moved_permanently, .found {
228 if method == Method.post {
229 Method.get
230 } else {
231 method
232 }
233 }
234 else {
235 method
236 }
237 }
238}
239
240fn (req &Request) method_and_url_to_response(method Method, url urllib.URL, data string, header Header) !Response {
241 host_name := url.hostname()
242 scheme := url.scheme
243 p := url.escaped_path().trim_left('/')
244 path := if url.query().len > 0 { '/${p}?${url.query().encode()}' } else { '/${p}' }
245 mut nport := url.port().int()
246 if nport == 0 {
247 if scheme == 'http' {
248 nport = 80
249 }
250 if scheme == 'https' {
251 nport = 443
252 }
253 }
254 // println('fetch ${method}, ${scheme}, ${host_name}, ${nport}, ${path} ')
255 if scheme == 'https' && req.proxy == unsafe { nil } {
256 // println('ssl_do( ${nport}, ${method}, ${host_name}, ${path} )')
257 for i in 0 .. req.max_retries {
258 res := req.ssl_do(nport, method, host_name, path, data, header) or {
259 if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) {
260 return err
261 }
262 continue
263 }
264 return res
265 }
266 } else if scheme == 'http' && req.proxy == unsafe { nil } {
267 // println('http_do( ${nport}, ${method}, ${host_name}, ${path} )')
268 for i in 0 .. req.max_retries {
269 res := req.http_do('${host_name}:${nport}', method, path, data, header) or {
270 if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) {
271 return err
272 }
273 continue
274 }
275 return res
276 }
277 } else if req.proxy != unsafe { nil } {
278 for i in 0 .. req.max_retries {
279 res := req.proxy.http_do(url, method, path, req, data, header) or {
280 if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) {
281 return err
282 }
283 continue
284 }
285 return res
286 }
287 }
288 return error('http.request.method_and_url_to_response: unsupported scheme: "${scheme}"')
289}
290
291fn (req &Request) build_request_headers(method Method, host_name string, port int, path string) string {
292 return req.build_request_headers_with(method, host_name, port, path, req.data, req.header)
293}
294
295fn (req &Request) build_request_headers_with(method Method, host_name string, port int, path string, data string, header Header) string {
296 mut sb := strings.new_builder(4096)
297 version := if req.version == .unknown { Version.v1_1 } else { req.version }
298 sb.write_string(method.str())
299 sb.write_string(' ')
300 sb.write_string(path)
301 sb.write_string(' ')
302 sb.write_string(version.str())
303 sb.write_string('\r\n')
304 if !header.contains(.host) {
305 sb.write_string('Host: ')
306 if port != 80 && port != 443 && port != 0 {
307 sb.write_string('${host_name}:${port}')
308 } else {
309 sb.write_string(host_name)
310 }
311 sb.write_string('\r\n')
312 }
313 if !header.contains(.user_agent) {
314 ua := req.user_agent
315 sb.write_string('User-Agent: ')
316 sb.write_string(ua)
317 sb.write_string('\r\n')
318 }
319 if !header.contains(.content_length) {
320 // Write Content-Length: 0 even if there's no content, since some APIs
321 // stop working without this header.
322 sb.write_string('Content-Length: ')
323 sb.write_string(data.len.str())
324 sb.write_string('\r\n')
325 }
326 chkey := CommonHeader.cookie.str()
327 for key in header.keys() {
328 if key == chkey {
329 continue
330 }
331 val := header.custom_values(key).join('; ')
332 sb.write_string(key)
333 sb.write_string(': ')
334 sb.write_string(val)
335 sb.write_string('\r\n')
336 }
337 sb.write_string(req.build_request_cookies_header_with_header(header))
338 sb.write_string('Connection: close\r\n')
339 sb.write_string('\r\n')
340 sb.write_string(data)
341 return sb.str()
342}
343
344fn (req &Request) build_request_cookies_header() string {
345 return req.build_request_cookies_header_with_header(req.header)
346}
347
348fn (req &Request) build_request_cookies_header_with_header(header Header) string {
349 if req.cookies.len < 1 {
350 return ''
351 }
352 mut sb_cookie := strings.new_builder(1024)
353 hvcookies := header.values(.cookie)
354 total_cookies := req.cookies.len + hvcookies.len
355 sb_cookie.write_string('Cookie: ')
356 mut idx := 0
357 for key, val in req.cookies {
358 sb_cookie.write_string(key)
359 sb_cookie.write_string('=')
360 sb_cookie.write_string(val)
361 if idx < total_cookies - 1 {
362 sb_cookie.write_string('; ')
363 }
364 idx++
365 }
366 for c in hvcookies {
367 sb_cookie.write_string(c)
368 if idx < total_cookies - 1 {
369 sb_cookie.write_string('; ')
370 }
371 idx++
372 }
373 sb_cookie.write_string('\r\n')
374 return sb_cookie.str()
375}
376
377fn (req &Request) http_do(host string, method Method, path string, data string, header Header) !Response {
378 host_name, port := net.split_address(host)!
379 s := req.build_request_headers_with(method, host_name, port, path, data, header)
380 mut client := net.dial_tcp(host)!
381 client.set_read_timeout(req.read_timeout)
382 client.set_write_timeout(req.write_timeout)
383 // TODO: this really needs to be exposed somehow
384 client.write(s.bytes())!
385 $if trace_http_request ? {
386 eprint('> ')
387 eprint(s)
388 eprintln('')
389 }
390 response_data := req.read_all_from_client_connection(client)!
391 client.close()!
392 response_text := response_data.data.bytestr()
393 $if trace_http_response ? {
394 eprint('< ')
395 eprint(response_text)
396 eprintln('')
397 }
398 if req.on_finish != unsafe { nil } {
399 req.on_finish(req, u64(response_text.len))!
400 }
401 return parse_received_response(response_text, response_data.info)
402}
403
404// abstract over reading the whole content from TCP or SSL connections:
405type FnReceiveChunk = fn (con voidptr, buf &u8, bufsize int) !int
406
407enum ChunkedBodyTrackerState {
408 chunk_size
409 chunk_data
410 chunk_data_crlf_start
411 chunk_data_crlf_end
412 trailer_line
413}
414
415struct ChunkedBodyTracker {
416mut:
417 state ChunkedBodyTrackerState = .chunk_size
418 line_buf []u8
419 chunk_left u64
420 decoded_len u64
421 complete bool
422 invalid bool
423}
424
425fn (mut tracker ChunkedBodyTracker) advance(data []u8, mut decoded []u8) bool {
426 if tracker.complete || tracker.invalid || data.len == 0 {
427 return tracker.complete
428 }
429 mut i := 0
430 for i < data.len {
431 match tracker.state {
432 .chunk_size {
433 ch := data[i]
434 i++
435 if ch == `\r` {
436 continue
437 }
438 if ch != `\n` {
439 tracker.line_buf << ch
440 continue
441 }
442 chunk_size := parse_chunked_size_line(tracker.line_buf) or {
443 tracker.invalid = true
444 return false
445 }
446 tracker.line_buf.clear()
447 if chunk_size == 0 {
448 tracker.state = .trailer_line
449 continue
450 }
451 tracker.chunk_left = chunk_size
452 tracker.state = .chunk_data
453 }
454 .chunk_data {
455 available := data.len - i
456 if tracker.chunk_left < u64(available) {
457 decoded << data[i..i + int(tracker.chunk_left)]
458 tracker.decoded_len += tracker.chunk_left
459 i += int(tracker.chunk_left)
460 tracker.chunk_left = 0
461 } else {
462 decoded << data[i..]
463 tracker.decoded_len += u64(available)
464 tracker.chunk_left -= u64(available)
465 i = data.len
466 }
467 if tracker.chunk_left == 0 {
468 tracker.state = .chunk_data_crlf_start
469 }
470 }
471 .chunk_data_crlf_start {
472 if data[i] != `\r` {
473 tracker.invalid = true
474 return false
475 }
476 i++
477 tracker.state = .chunk_data_crlf_end
478 }
479 .chunk_data_crlf_end {
480 if data[i] != `\n` {
481 tracker.invalid = true
482 return false
483 }
484 i++
485 tracker.state = .chunk_size
486 }
487 .trailer_line {
488 ch := data[i]
489 i++
490 if ch == `\r` {
491 continue
492 }
493 if ch != `\n` {
494 tracker.line_buf << ch
495 continue
496 }
497 if tracker.line_buf.len == 0 {
498 tracker.complete = true
499 return true
500 }
501 tracker.line_buf.clear()
502 }
503 }
504 }
505 return tracker.complete
506}
507
508fn parse_chunked_size_line(line []u8) !u64 {
509 mut size := u64(0)
510 mut has_digit := false
511 for ch in line {
512 if ch == `;` {
513 break
514 }
515 if !ch.is_hex_digit() {
516 return error('invalid chunk size')
517 }
518 has_digit = true
519 size = (size << 4) | u64(chunked_hex_value(ch))
520 }
521 if !has_digit {
522 return error('invalid chunk size')
523 }
524 return size
525}
526
527fn chunked_hex_value(ch u8) u8 {
528 if `0` <= ch && ch <= `9` {
529 return ch - `0`
530 }
531 if `a` <= ch && ch <= `f` {
532 return ch - `a` + 10
533 }
534 if `A` <= ch && ch <= `F` {
535 return ch - `A` + 10
536 }
537 return 0
538}
539
540struct ReceivedResponseInfo {
541 headers_end int = -1
542 is_chunked_transfer bool
543 has_truncated_body bool
544}
545
546fn parse_received_response(response_text string, info ReceivedResponseInfo) !Response {
547 if info.is_chunked_transfer && info.has_truncated_body && info.headers_end > 0
548 && info.headers_end <= response_text.len {
549 return parse_response(response_text[..info.headers_end])
550 }
551 return parse_response(response_text)
552}
553
554// response_has_no_body returns true when the HTTP method or status code
555// guarantees that no response body is sent (HEAD requests, 1xx informational,
556// 204 No Content, 304 Not Modified). For these, a `Content-Length` header
557// describes the body that *would* have been sent for a GET, so it must not
558// drive read termination or completion validation. (RFC 7230 §3.3.3)
559fn response_has_no_body(method Method, status_code int) bool {
560 if method == .head {
561 return true
562 }
563 return status_code in [101, 102, 103, 204, 304]
564}
565
566fn validate_received_response_completion(has_content_length bool, expected_size u64, body_so_far u64, is_chunked_transfer bool, chunked_complete bool) ! {
567 if has_content_length && body_so_far < expected_size {
568 return error('http.request: response body ended early: received ${body_so_far} of ${expected_size} bytes')
569 }
570 if is_chunked_transfer && !chunked_complete {
571 return error('http.request: incomplete chunked response')
572 }
573}
574
575fn (req &Request) receive_all_data_from_cb_in_builder(mut content strings.Builder, con voidptr, receive_chunk_cb FnReceiveChunk) !ReceivedResponseInfo {
576 mut buff := [bufsize]u8{}
577 bp := unsafe { &buff[0] }
578 mut readcounter := 0
579 mut body_pos := u64(0)
580 mut headers_end := -1
581 mut expected_size := u64(0)
582 mut has_content_length := false
583 mut is_chunked_transfer := false
584 mut chunked_body_tracker := ChunkedBodyTracker{}
585 mut header_buf := strings.new_builder(1024)
586 mut old_len := u64(0)
587 mut new_len := u64(0)
588 mut status_code := -1
589 mut has_truncated_body := false
590 for {
591 readcounter++
592 len := receive_chunk_cb(con, bp, bufsize) or {
593 if err is io.Eof {
594 body_so_far := if headers_end >= 0 && old_len > body_pos {
595 old_len - body_pos
596 } else {
597 u64(0)
598 }
599 if !response_has_no_body(req.method, status_code) {
600 validate_received_response_completion(has_content_length, expected_size,
601 body_so_far, is_chunked_transfer, chunked_body_tracker.complete)!
602 }
603 break
604 }
605 return err
606 }
607 $if debug_http ? {
608 eprintln('ssl_do, read ${readcounter:4d} | len: ${len}')
609 eprintln('-'.repeat(20))
610 eprintln(unsafe { tos(bp, len) })
611 eprintln('-'.repeat(20))
612 }
613 if len <= 0 {
614 body_so_far := if headers_end >= 0 && old_len > body_pos {
615 old_len - body_pos
616 } else {
617 u64(0)
618 }
619 if !response_has_no_body(req.method, status_code) {
620 validate_received_response_completion(has_content_length, expected_size,
621 body_so_far, is_chunked_transfer, chunked_body_tracker.complete)!
622 }
623 break
624 }
625 new_len = old_len + u64(len)
626 // Note: `schunk` and `bchunk` are used as convenient stack located views to the currently filled part of `buff`:
627 schunk := unsafe { bp.vstring_literal_with_len(len) }
628 mut bchunk := unsafe { bp.vbytes(len) }
629 if readcounter == 1 {
630 http_line := schunk.all_before('\r\n')
631 status_code = http_line.all_after(' ').all_before(' ').int()
632 }
633 if req.on_progress != unsafe { nil } {
634 req.on_progress(req, bchunk, u64(new_len))!
635 }
636 if headers_end < 0 {
637 unsafe { header_buf.write_ptr(bp, len) }
638 if header_buf.len >= headers_body_boundary.len {
639 header_str := header_buf.bytestr()
640 hidx := header_str.index_(headers_body_boundary)
641 if hidx >= 0 {
642 headers_end = hidx + headers_body_boundary.len
643 body_pos = u64(headers_end)
644 for line in header_str[..hidx].split('\r\n') {
645 if line.len == 0 {
646 continue
647 }
648 low := line.to_lower()
649 if low.starts_with('content-length:') {
650 raw_cl := line.all_after(':').trim_space()
651 mut valid_cl := raw_cl.len > 0
652 for ch in raw_cl {
653 if !ch.is_digit() {
654 valid_cl = false
655 break
656 }
657 }
658 if valid_cl {
659 expected_size = raw_cl.u64()
660 has_content_length = true
661 }
662 } else if low.starts_with('transfer-encoding:')
663 && has_header_token(line.all_after(':').trim_space(), 'chunked') {
664 is_chunked_transfer = true
665 }
666 }
667 if is_chunked_transfer {
668 has_content_length = false
669 }
670 }
671 }
672 }
673 if headers_end >= 0 && old_len < u64(headers_end) {
674 header_bytes_in_chunk := int(u64(headers_end) - old_len)
675 if header_bytes_in_chunk >= len {
676 bchunk = []u8{}
677 } else {
678 bchunk = unsafe { (&u8(bchunk.data) + header_bytes_in_chunk).vbytes(len - header_bytes_in_chunk) }
679 }
680 }
681 mut body_so_far := u64(0)
682 if headers_end >= 0 && new_len > body_pos {
683 body_so_far = u64(new_len) - body_pos
684 }
685 mut progress_body_so_far := body_so_far
686 mut chunked_complete := false
687 if is_chunked_transfer {
688 mut dechunked := []u8{}
689 chunked_complete = chunked_body_tracker.advance(bchunk, mut dechunked)
690 progress_body_so_far = chunked_body_tracker.decoded_len
691 if req.on_progress_body != unsafe { nil } && dechunked.len > 0 {
692 req.on_progress_body(req, dechunked, progress_body_so_far, expected_size,
693 status_code)!
694 }
695 } else if req.on_progress_body != unsafe { nil } {
696 req.on_progress_body(req, bchunk, progress_body_so_far, expected_size, status_code)!
697 }
698 if !(req.stop_copying_limit > 0 && new_len > req.stop_copying_limit) {
699 unsafe { content.write_ptr(bp, len) }
700 } else if headers_end >= 0 && new_len > body_pos {
701 has_truncated_body = true
702 }
703 if is_chunked_transfer && chunked_complete {
704 break
705 }
706 if headers_end >= 0 && response_has_no_body(req.method, status_code) {
707 // HEAD / 1xx / 204 / 304: response body is forbidden by the spec, so
708 // stop as soon as the headers terminator is in. Any `Content-Length`
709 // describes a body that will never be sent.
710 break
711 }
712 if has_content_length {
713 if expected_size > 0 && body_so_far >= expected_size {
714 break
715 }
716 }
717 if req.stop_receiving_limit > 0 && new_len > req.stop_receiving_limit {
718 break
719 }
720 old_len = new_len
721 }
722 return ReceivedResponseInfo{
723 headers_end: headers_end
724 is_chunked_transfer: is_chunked_transfer
725 has_truncated_body: has_truncated_body
726 }
727}
728
729fn read_from_tcp_connection_cb(con voidptr, buf &u8, bufsize int) !int {
730 mut r := unsafe { &net.TcpConn(con) }
731 return r.read_ptr(buf, bufsize)
732}
733
734struct ReceivedResponseBuffer {
735 data []u8
736 info ReceivedResponseInfo
737}
738
739fn (req &Request) read_all_from_client_connection(r &net.TcpConn) !ReceivedResponseBuffer {
740 mut content := strings.new_builder(4096)
741 info := req.receive_all_data_from_cb_in_builder(mut content, voidptr(r),
742 read_from_tcp_connection_cb)!
743 return ReceivedResponseBuffer{
744 data: content
745 info: info
746 }
747}
748
749// referer returns 'Referer' header value of the given request
750pub fn (req &Request) referer() string {
751 return req.header.get(.referer) or { '' }
752}
753
754// parse_request parses a raw HTTP request into a Request object.
755// See also: `parse_request_head`, which parses only the headers.
756pub fn parse_request(mut reader io.BufferedReader) !Request {
757 mut request := parse_request_head(mut reader)!
758
759 // body
760 mut body := []u8{}
761 if length := request.header.get(.content_length) {
762 n := length.int()
763 if n > 0 {
764 body = []u8{len: n}
765 mut count := 0
766 for count < body.len {
767 count += reader.read(mut body[count..]) or { break }
768 }
769 }
770 }
771
772 request.data = body.bytestr()
773 return request
774}
775
776// parse_request_head parses *only* the header of a raw HTTP request into a Request object
777pub fn parse_request_head(mut reader io.BufferedReader) !Request {
778 // request line
779 mut line := reader.read_line()!
780 method, target, version := parse_request_line(line)!
781
782 // headers
783 mut header := new_header()
784 line = reader.read_line()!
785 for line != '' {
786 // key, value := parse_header(line)!
787 mut pos := parse_header_fast(line)!
788 key := line[..pos]
789 for pos < line.len - 1 && line[pos + 1].is_space() {
790 // Skip space or tab in value name
791 pos++
792 }
793 if pos + 1 < line.len {
794 value := line[pos + 1..]
795 _, _ = key, value
796 // println('key,value=${key},${value}')
797 header.add_custom(key, value)!
798 }
799 line = reader.read_line()!
800 }
801 // header.coerce(canonicalize: true)
802
803 mut request_cookies := map[string]string{}
804 for _, cookie in read_cookies(header, '') {
805 request_cookies[cookie.name] = cookie.value
806 }
807
808 return Request{
809 method: method
810 url: target.str()
811 header: header
812 host: (header.get(.host) or { '' }).clone()
813 version: version
814 cookies: request_cookies
815 }
816}
817
818// parse_request_head parses *only* the header of a raw HTTP request into a Request object
819pub fn parse_request_head_str(s string) !Request {
820 pos0 := s.index_('\n')
821 if pos0 == -1 {
822 return error('malformed request: no request line found')
823 }
824 line0 := s[..pos0].trim_space()
825 method, target, version := parse_request_line_fast(line0)!
826
827 // headers
828 mut header := new_header()
829 mut line_start := pos0 + 1
830 for line_start < s.len {
831 mut line_end := s.index_after_('\n', line_start)
832 if line_end == -1 {
833 line_end = s.len
834 }
835 mut line := s[line_start..line_end]
836 if line.len > 0 && line[line.len - 1] == `\r` {
837 line = line[..line.len - 1]
838 }
839 // IMPORTANT: HTTP headers end at the first empty line.
840 // If we hit this, we are now at the body, so we stop parsing headers.
841 if line == '' {
842 break
843 }
844 mut pos := parse_header_fast(line) or {
845 line_start = line_end + 1
846 continue
847 }
848 key := line[..pos]
849
850 // Skip space or tab after the colon
851 mut val_start := pos + 1
852 for val_start < line.len && line[val_start].is_space() {
853 val_start++
854 }
855
856 if val_start < line.len {
857 value := line[val_start..]
858 header.add_custom(key, value)!
859 }
860 line_start = line_end + 1
861 }
862
863 mut request_cookies := map[string]string{}
864 for _, cookie in read_cookies(header, '') {
865 request_cookies[cookie.name] = cookie.value
866 }
867
868 return Request{
869 method: method
870 url: target
871 header: header
872 host: (header.get(.host) or { '' }).clone()
873 version: version
874 cookies: request_cookies
875 }
876}
877
878fn parse_request_line_fast(line string) !(Method, string, Version) {
879 space1 := line.index_u8(` `)
880 if space1 <= 0 {
881 return error('bad request header')
882 }
883 space2_rel := line.index_after_(' ', space1 + 1)
884 if space2_rel == -1 || space2_rel == space1 + 1 || space2_rel >= line.len - 1 {
885 return error('bad request header')
886 }
887 method := method_from_str(line[..space1])
888 version := version_from_str(line[space2_rel + 1..])
889 if version == .unknown {
890 return error('unsupported version')
891 }
892 return method, line[space1 + 1..space2_rel], version
893}
894
895const headers_body_boundary = '\r\n\r\n'
896
897// parse_request_str parses a raw HTTP request string into a Request object.
898pub fn parse_request_str(s string) !Request {
899 mut request := parse_request_head_str(s)!
900 body_pos := s.index_(headers_body_boundary)
901 if body_pos != -1 {
902 request.data = s[body_pos + headers_body_boundary.len..]
903 }
904 return request
905}
906
907fn parse_request_line(line string) !(Method, urllib.URL, Version) {
908 // println('S=${s}')
909 words := line.split(' ')
910 // println('words=')
911 // println(words)
912 if words.len != 3 {
913 return error('bad request header')
914 }
915 method_str, target_str, version_str := words[0], words[1], words[2]
916
917 /*
918 space1, space2 := fast_request_words(line)
919 // if words.len != 3 {
920 if space1 == 0 || space2 == 0 {
921 return error('malformed request line')
922 }
923 method_str := s.substr_unsafe(0, space1)
924 target_str := s.substr_unsafe(space1 + 1, space2)
925 version_str := s.substr_unsafe(space2 + 1, s.len)
926 */
927 // println('${method_str}!${target_str}!${version_str}')
928 // method := method_from_str(words[0])
929 // target := urllib.parse(words[1])!
930 // version := version_from_str(words[2])
931 method := method_from_str(method_str)
932 target := urllib.parse_request_uri(target_str)!
933 // println('before version_str="${version_str}"')
934 version := version_from_str(version_str)
935 // println('VERSION="${version}"')
936 if version == .unknown {
937 return error('unsupported version')
938 }
939 return method, target, version
940}
941
942// Parse URL encoded key=value&key=value forms
943//
944// FIXME: Some servers can require the
945// parameter in a specific order.
946//
947// a possible solution is to use the a list of QueryValue
948pub fn parse_form(body string) map[string]string {
949 mut form := map[string]string{}
950
951 if body.match_glob('{*}') {
952 form['json'] = body
953 } else {
954 words := body.split('&')
955
956 for word in words {
957 kv := word.split_nth('=', 2)
958 if kv.len != 2 {
959 continue
960 }
961 key := urllib.query_unescape(kv[0]) or { continue }
962 val := urllib.query_unescape(kv[1]) or { continue }
963 form[key] = val
964 }
965 }
966 return form
967 // }
968 // todo: parse form-data and application/json
969 // ...
970}
971
972pub struct FileData {
973pub:
974 filename string
975 content_type string
976 data string
977}
978
979pub struct UnexpectedExtraAttributeError {
980 Error
981pub:
982 attributes []string
983}
984
985pub fn (err UnexpectedExtraAttributeError) msg() string {
986 return 'Encountered unexpected extra attributes: ${err.attributes}'
987}
988
989pub struct MultiplePathAttributesError {
990 Error
991}
992
993pub fn (err MultiplePathAttributesError) msg() string {
994 return 'Expected at most one path attribute'
995}
996
997// multipart_form_body converts form and file data into a multipart/form
998// HTTP request body. It is the inverse of parse_multipart_form. Returns
999// (body, boundary).
1000// Note: Form keys should not contain quotes
1001fn multipart_form_body(form map[string]string, files map[string][]FileData) (string, string) {
1002 rboundary := rand.ulid()
1003 mut sb := strings.new_builder(1024)
1004 for name, value in form {
1005 sb.write_string('\r\n--')
1006 sb.write_string(rboundary)
1007 sb.write_string('\r\nContent-Disposition: form-data; name="')
1008 sb.write_string(name)
1009 sb.write_string('"\r\n\r\n')
1010 sb.write_string(value)
1011 }
1012 for name, fs in files {
1013 for f in fs {
1014 sb.write_string('\r\n--')
1015 sb.write_string(rboundary)
1016 sb.write_string('\r\nContent-Disposition: form-data; name="')
1017 sb.write_string(name)
1018 sb.write_string('"; filename="')
1019 sb.write_string(f.filename)
1020 sb.write_string('"\r\nContent-Type: ')
1021 sb.write_string(f.content_type)
1022 sb.write_string('\r\n\r\n')
1023 sb.write_string(f.data)
1024 }
1025 }
1026 sb.write_string('\r\n--')
1027 sb.write_string(rboundary)
1028 sb.write_string('--')
1029 return sb.str(), rboundary
1030}
1031
1032struct LineSegmentIndexes {
1033mut:
1034 start int
1035 end int
1036}
1037
1038// parse_multipart_form parses an http request body, given a boundary string
1039// For more details about multipart forms, see:
1040// https://datatracker.ietf.org/doc/html/rfc2183
1041// https://datatracker.ietf.org/doc/html/rfc2388
1042// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
1043pub fn parse_multipart_form(body string, boundary string) (map[string]string, map[string][]FileData) {
1044 // dump(body)
1045 // dump(boundary)
1046 mut form := map[string]string{}
1047 mut files := map[string][]FileData{}
1048 if body.len == 0 || boundary.len == 0 || boundary.len > body.len {
1049 return form, files
1050 }
1051 mut field_start := body.index_after_(boundary, 0)
1052 if field_start == -1 {
1053 return form, files
1054 }
1055 field_start += boundary.len
1056 mut line_segments := []LineSegmentIndexes{cap: 100}
1057 for {
1058 if field_start > body.len - boundary.len {
1059 break
1060 }
1061 field_end := body.index_after_(boundary, field_start)
1062 if field_end == -1 {
1063 break
1064 }
1065 line_segments.clear()
1066 mut line_idx, mut line_start := 0, field_start
1067 for cidx := field_start; cidx < field_end; cidx++ {
1068 if line_idx >= 6 {
1069 // no need to scan further
1070 break
1071 }
1072 if body[cidx] == `\n` {
1073 line_segments << LineSegmentIndexes{line_start, cidx}
1074 line_start = cidx + 1
1075 line_idx++
1076 }
1077 }
1078 line_segments << LineSegmentIndexes{line_start, field_end}
1079 field_start = field_end + boundary.len
1080 if line_segments.len < 2 {
1081 continue
1082 }
1083 line1 := body#[line_segments[1].start..line_segments[1].end]
1084 line2 := if line_segments.len == 2 {
1085 ''
1086 } else {
1087 body#[line_segments[2].start..line_segments[2].end]
1088 }
1089 disposition := parse_disposition(line1.trim_space())
1090 // Grab everything between the double quotes
1091 name := disposition['name'] or { continue }
1092 // Parse files
1093 // TODO: handle `filename*`, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
1094 if filename := disposition['filename'] {
1095 // reject early broken content
1096 if line_segments.len < 5 {
1097 continue
1098 }
1099 // reject early non Content-Type headers
1100 if !line2.to_lower().starts_with('content-type:') {
1101 continue
1102 }
1103 content_type := line2.split_nth(':', 2)[1].trim_space()
1104 // line1: Content-Disposition: form-data; name="upfile"; filename="photo123.jpg"
1105 // line2: Content-Type: image/jpeg
1106 // line3:
1107 // line4: DATA
1108 // ...
1109 // lineX: --
1110 data_end := field_end - 4 // each multipart field ends with \r\n--
1111 if data_end < line_segments[4].start {
1112 continue
1113 }
1114 data := body[line_segments[4].start..data_end]
1115 // dump(data.limit(20).bytes())
1116 // dump(data.len)
1117 if name !in files {
1118 files[name] = []FileData{}
1119 }
1120 files[name] << FileData{
1121 filename: filename
1122 content_type: content_type
1123 data: data
1124 }
1125 continue
1126 }
1127 if line_segments.len < 4 {
1128 continue
1129 }
1130 data_end := field_end - 4
1131 if data_end < line_segments[3].start {
1132 continue
1133 }
1134 form[name] = body[line_segments[3].start..data_end]
1135 }
1136 // dump(form)
1137 return form, files
1138}
1139
1140// parse_disposition parses the Content-Disposition header of a multipart form.
1141// Returns a map of the key="value" pairs
1142// Example: assert parse_disposition('Content-Disposition: form-data; name="a"; filename="b"') == {'name': 'a', 'filename': 'b'}
1143fn parse_disposition(line string) map[string]string {
1144 mut data := map[string]string{}
1145 for word in line.split(';') {
1146 kv := word.split_nth('=', 2)
1147 if kv.len != 2 {
1148 continue
1149 }
1150 key, value := kv[0].to_lower().trim_left(' \t'), kv[1]
1151 if value.starts_with('"') && value.ends_with('"') {
1152 data[key] = value[1..value.len - 1]
1153 } else {
1154 data[key] = value
1155 }
1156 }
1157 return data
1158}
1159
1160fn is_no_need_retry_error(err_code int) bool {
1161 return err_code in [
1162 net.err_port_out_of_range.code(),
1163 net.err_no_udp_remote.code(),
1164 net.err_connect_timed_out.code(),
1165 net.err_timed_out_code,
1166 ]
1167}
1168