v2 / vlib / net / http / response.v
235 lines · 215 sloc · 6.08 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 compress.gzip
7import compress.zlib
8import net.http.chunked
9import strconv
10import strings
11
12// Response represents the result of the request
13pub struct Response {
14pub mut:
15 body string
16 header Header
17 status_code int
18 status_msg string
19 http_version string
20}
21
22fn (mut resp Response) free() {
23 unsafe { resp.header.free() }
24}
25
26// Formats resp to bytes suitable for HTTP response transmission
27pub fn (resp Response) bytes() []u8 {
28 mut sb := strings.new_builder(resp.response_buffer_cap())
29 resp.write_into_builder(mut sb)
30 return unsafe { sb.reuse_as_plain_u8_array() }
31}
32
33// Formats resp to a string suitable for HTTP response transmission
34pub fn (resp Response) bytestr() string {
35 mut sb := strings.new_builder(resp.response_buffer_cap())
36 resp.write_into_builder(mut sb)
37 res := sb.str()
38 unsafe { sb.free() }
39 return res
40}
41
42fn (resp Response) response_buffer_cap() int {
43 return resp.body.len + 64 + resp.header.cur_pos * 48
44}
45
46fn (resp Response) write_into_builder(mut sb strings.Builder) {
47 sb.write_string('HTTP/')
48 sb.write_string(resp.http_version)
49 sb.write_u8(` `)
50 sb.write_decimal(resp.status_code)
51 sb.write_u8(` `)
52 sb.write_string(resp.status_msg)
53 sb.write_string('\r\n')
54 resp.header.render_into_sb(mut sb,
55 version: resp.version()
56 )
57 sb.write_string('\r\n')
58 sb.write_string(resp.body)
59}
60
61// Parse a raw HTTP response into a Response object
62pub fn parse_response(resp string) !Response {
63 version, status_code, status_msg := parse_status_line(resp.all_before('\r\n'))!
64 // Build resp header map and separate the body
65 start_idx, end_idx := find_headers_range(resp)!
66 header := parse_headers(resp.substr(start_idx, end_idx))!
67 mut body := resp.substr(end_idx, resp.len)
68 if has_header_token(header.get(.transfer_encoding) or { '' }, 'chunked') {
69 body = chunked.decode(body)!
70 }
71 body = decode_response_body(body, header.get(.content_encoding) or { '' })
72 return Response{
73 http_version: version
74 status_code: status_code
75 status_msg: status_msg
76 header: header
77 body: body
78 }
79}
80
81fn has_header_token(header_value string, expected_token string) bool {
82 for token in parse_header_tokens(header_value) {
83 if token == expected_token.to_lower() {
84 return true
85 }
86 }
87 return false
88}
89
90fn parse_header_tokens(header_value string) []string {
91 mut tokens := []string{}
92 for part in header_value.split(',') {
93 token := part.all_before(';').trim_space().to_lower()
94 if token != '' {
95 tokens << token
96 }
97 }
98 return tokens
99}
100
101fn decode_response_body(body string, content_encoding string) string {
102 if body.len == 0 {
103 return body
104 }
105 encodings := parse_header_tokens(content_encoding)
106 if encodings.len == 0 {
107 return body
108 }
109 mut decoded := body.bytes()
110 for i := encodings.len - 1; i >= 0; i-- {
111 encoding := encodings[i]
112 decoded = match encoding {
113 'gzip', 'x-gzip' {
114 gzip.decompress(decoded) or { return body }
115 }
116 'deflate' {
117 zlib.decompress(decoded) or { return body }
118 }
119 'identity' {
120 decoded
121 }
122 else {
123 return body
124 }
125 }
126 }
127 return decoded.bytestr()
128}
129
130// parse_status_line parses the first HTTP response line into the HTTP
131// version, status code, and reason phrase
132fn parse_status_line(line string) !(string, int, string) {
133 if line.len < 5 || line[..5].to_lower() != 'http/' {
134 return error('response does not start with HTTP/, line: `${line}`')
135 }
136 data := line.split_nth(' ', 3)
137 if data.len != 3 {
138 return error('expected at least 3 tokens, but found: ${data.len}')
139 }
140 version := data[0].substr(5, data[0].len)
141 // validate version is 1*DIGIT "." 1*DIGIT
142 digits := version.split_nth('.', 3)
143 if digits.len != 2 {
144 return error('HTTP version malformed, found: `${digits}`')
145 }
146 for digit in digits {
147 strconv.atoi(digit) or {
148 return error('HTTP version must contain only integers, found: `${digit}`')
149 }
150 }
151 return version, strconv.atoi(data[1])!, data[2]
152}
153
154// cookies parses the Set-Cookie headers into Cookie objects
155pub fn (r Response) cookies() []Cookie {
156 mut cookies := []Cookie{}
157 for cookie in r.header.values(.set_cookie) {
158 cookies << parse_cookie(cookie) or { continue }
159 }
160 return cookies
161}
162
163// status parses the status_code and returns a corresponding enum field of Status
164pub fn (r Response) status() Status {
165 return status_from_int(r.status_code)
166}
167
168// set_status sets the status_code and status_msg of the response
169pub fn (mut r Response) set_status(s Status) {
170 r.status_code = s.int()
171 r.status_msg = s.str()
172}
173
174// version parses the version
175pub fn (r Response) version() Version {
176 return match r.http_version {
177 '1.0' { .v1_0 }
178 '1.1' { .v1_1 }
179 '2.0' { .v2_0 }
180 else { .unknown }
181 }
182}
183
184// set_version sets the http_version string of the response
185pub fn (mut r Response) set_version(v Version) {
186 if v == .unknown {
187 r.http_version = ''
188 return
189 }
190 maj, min := v.protos()
191 r.http_version = '${maj}.${min}'
192}
193
194pub struct ResponseConfig {
195pub:
196 version Version = .v1_1
197 status Status = .ok
198 header Header
199 body string
200}
201
202// new_response creates a Response object from the configuration. This
203// function will add a Content-Length header if body is not empty.
204pub fn new_response(conf ResponseConfig) Response {
205 mut resp := Response{
206 body: conf.body
207 header: conf.header
208 }
209 if resp.body != '' && !resp.header.contains(.content_length) {
210 resp.header.add(.content_length, resp.body.len.str())
211 }
212 resp.set_status(conf.status)
213 resp.set_version(conf.version)
214 return resp
215}
216
217// find_headers_range returns the start (inclusive) and end (exclusive)
218// index of the headers in the string, including the trailing newlines. This
219// helper function expects the first line in `data` to be the HTTP status line
220// (HTTP/1.1 200 OK).
221fn find_headers_range(data string) !(int, int) {
222 start_idx := data.index('\n') or { return error('no start index found') } + 1
223 mut count := 0
224 for i := start_idx; i < data.len; i++ {
225 if data[i] == `\n` {
226 count++
227 } else if data[i] != `\r` {
228 count = 0
229 }
230 if count == 2 {
231 return start_idx, i + 1
232 }
233 }
234 return error('no end index found')
235}
236