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