2023-03-28 23:55:57 +03:00
|
|
|
// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
|
2021-06-06 00:43:14 +03:00
|
|
|
// Use of this source code is governed by an MIT license
|
|
|
|
// that can be found in the LICENSE file.
|
|
|
|
module http
|
|
|
|
|
2021-06-14 10:08:41 +03:00
|
|
|
import net.http.chunked
|
2021-07-24 20:47:45 +03:00
|
|
|
import strconv
|
2021-06-06 00:43:14 +03:00
|
|
|
|
|
|
|
// Response represents the result of the request
|
|
|
|
pub struct Response {
|
|
|
|
pub mut:
|
2022-05-29 20:27:18 +03:00
|
|
|
body string
|
|
|
|
text string [deprecated: 'use Response.body instead'; deprecated_after: '2022-10-03']
|
2021-07-24 20:47:45 +03:00
|
|
|
header Header
|
|
|
|
status_code int
|
|
|
|
status_msg string
|
|
|
|
http_version string
|
2021-06-06 00:43:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fn (mut resp Response) free() {
|
2021-07-24 20:47:45 +03:00
|
|
|
unsafe { resp.header.free() }
|
2021-06-06 00:43:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Formats resp to bytes suitable for HTTP response transmission
|
2022-04-15 15:35:35 +03:00
|
|
|
pub fn (resp Response) bytes() []u8 {
|
|
|
|
// TODO: build []u8 directly; this uses two allocations
|
2021-07-10 11:58:07 +03:00
|
|
|
return resp.bytestr().bytes()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Formats resp to a string suitable for HTTP response transmission
|
|
|
|
pub fn (resp Response) bytestr() string {
|
2022-11-15 16:53:13 +03:00
|
|
|
return 'HTTP/${resp.http_version} ${resp.status_code} ${resp.status_msg}\r\n' + '${resp.header.render(
|
2021-07-24 20:47:45 +03:00
|
|
|
version: resp.version()
|
2022-08-08 04:36:45 +03:00
|
|
|
)}\r\n' + resp.body
|
2021-06-06 00:43:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse a raw HTTP response into a Response object
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn parse_response(resp string) !Response {
|
2022-11-20 17:21:50 +03:00
|
|
|
version, status_code, status_msg := parse_status_line(resp.all_before('\r\n'))!
|
2021-06-06 00:43:14 +03:00
|
|
|
// Build resp header map and separate the body
|
2022-10-16 09:28:57 +03:00
|
|
|
start_idx, end_idx := find_headers_range(resp)!
|
|
|
|
header := parse_headers(resp.substr(start_idx, end_idx))!
|
2022-05-29 20:27:18 +03:00
|
|
|
mut body := resp.substr(end_idx, resp.len)
|
2021-06-15 19:28:54 +03:00
|
|
|
if header.get(.transfer_encoding) or { '' } == 'chunked' {
|
2022-05-29 20:27:18 +03:00
|
|
|
body = chunked.decode(body)
|
2021-06-06 00:43:14 +03:00
|
|
|
}
|
|
|
|
return Response{
|
2021-07-24 20:47:45 +03:00
|
|
|
http_version: version
|
2021-06-06 00:43:14 +03:00
|
|
|
status_code: status_code
|
2021-07-24 20:47:45 +03:00
|
|
|
status_msg: status_msg
|
2021-06-06 00:43:14 +03:00
|
|
|
header: header
|
2022-05-29 20:27:18 +03:00
|
|
|
body: body
|
|
|
|
text: body // TODO: remove as depreciated
|
2021-06-06 00:43:14 +03:00
|
|
|
}
|
|
|
|
}
|
2021-07-24 11:31:33 +03:00
|
|
|
|
2021-07-24 20:47:45 +03:00
|
|
|
// parse_status_line parses the first HTTP response line into the HTTP
|
|
|
|
// version, status code, and reason phrase
|
2022-10-16 09:28:57 +03:00
|
|
|
fn parse_status_line(line string) !(string, int, string) {
|
2021-07-24 20:47:45 +03:00
|
|
|
if line.len < 5 || line[..5].to_lower() != 'http/' {
|
2023-01-31 16:11:21 +03:00
|
|
|
return error('response does not start with HTTP/, line: `${line}`')
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
data := line.split_nth(' ', 3)
|
|
|
|
if data.len != 3 {
|
2023-01-31 16:11:21 +03:00
|
|
|
return error('expected at least 3 tokens, but found: ${data.len}')
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
version := data[0].substr(5, data[0].len)
|
|
|
|
// validate version is 1*DIGIT "." 1*DIGIT
|
|
|
|
digits := version.split_nth('.', 3)
|
|
|
|
if digits.len != 2 {
|
2023-01-31 16:11:21 +03:00
|
|
|
return error('HTTP version malformed, found: `${digits}`')
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
for digit in digits {
|
2023-01-31 16:11:21 +03:00
|
|
|
strconv.atoi(digit) or {
|
|
|
|
return error('HTTP version must contain only integers, found: `${digit}`')
|
|
|
|
}
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
return version, strconv.atoi(data[1])!, data[2]
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// cookies parses the Set-Cookie headers into Cookie objects
|
|
|
|
pub fn (r Response) cookies() []Cookie {
|
|
|
|
mut cookies := []Cookie{}
|
|
|
|
for cookie in r.header.values(.set_cookie) {
|
|
|
|
cookies << parse_cookie(cookie) or { continue }
|
|
|
|
}
|
|
|
|
return cookies
|
|
|
|
}
|
|
|
|
|
|
|
|
// status parses the status_code into a Status struct
|
|
|
|
pub fn (r Response) status() Status {
|
|
|
|
return status_from_int(r.status_code)
|
|
|
|
}
|
|
|
|
|
|
|
|
// set_status sets the status_code and status_msg of the response
|
|
|
|
pub fn (mut r Response) set_status(s Status) {
|
|
|
|
r.status_code = s.int()
|
|
|
|
r.status_msg = s.str()
|
|
|
|
}
|
|
|
|
|
|
|
|
// version parses the version
|
|
|
|
pub fn (r Response) version() Version {
|
2022-11-15 16:53:13 +03:00
|
|
|
return version_from_str('HTTP/${r.http_version}')
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// set_version sets the http_version string of the response
|
|
|
|
pub fn (mut r Response) set_version(v Version) {
|
|
|
|
if v == .unknown {
|
|
|
|
r.http_version = ''
|
|
|
|
return
|
|
|
|
}
|
|
|
|
maj, min := v.protos()
|
2022-11-15 16:53:13 +03:00
|
|
|
r.http_version = '${maj}.${min}'
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ResponseConfig {
|
|
|
|
version Version = .v1_1
|
|
|
|
status Status = .ok
|
|
|
|
header Header
|
2022-05-29 20:27:18 +03:00
|
|
|
body string
|
|
|
|
text string [deprecated: 'use ResponseConfig.body instead'; deprecated_after: '2022-10-03']
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// new_response creates a Response object from the configuration. This
|
2022-05-29 20:27:18 +03:00
|
|
|
// function will add a Content-Length header if body is not empty.
|
2021-07-24 20:47:45 +03:00
|
|
|
pub fn new_response(conf ResponseConfig) Response {
|
|
|
|
mut resp := Response{
|
2022-05-29 20:27:18 +03:00
|
|
|
body: conf.body + conf.text
|
2021-07-24 20:47:45 +03:00
|
|
|
header: conf.header
|
|
|
|
}
|
2022-05-29 20:27:18 +03:00
|
|
|
if resp.body.len > 0 && !resp.header.contains(.content_length) {
|
|
|
|
resp.header.add(.content_length, resp.body.len.str())
|
2021-07-24 20:47:45 +03:00
|
|
|
}
|
|
|
|
resp.set_status(conf.status)
|
|
|
|
resp.set_version(conf.version)
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2021-07-24 11:31:33 +03:00
|
|
|
// find_headers_range returns the start (inclusive) and end (exclusive)
|
|
|
|
// index of the headers in the string, including the trailing newlines. This
|
|
|
|
// helper function expects the first line in `data` to be the HTTP status line
|
|
|
|
// (HTTP/1.1 200 OK).
|
2022-10-16 09:28:57 +03:00
|
|
|
fn find_headers_range(data string) !(int, int) {
|
2021-07-24 11:31:33 +03:00
|
|
|
start_idx := data.index('\n') or { return error('no start index found') } + 1
|
|
|
|
mut count := 0
|
|
|
|
for i := start_idx; i < data.len; i++ {
|
|
|
|
if data[i] == `\n` {
|
|
|
|
count++
|
|
|
|
} else if data[i] != `\r` {
|
|
|
|
count = 0
|
|
|
|
}
|
|
|
|
if count == 2 {
|
|
|
|
return start_idx, i + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return error('no end index found')
|
|
|
|
}
|