aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/net/http/response.v
blob: caa8228e64966cfcd9ab3a12ea9ddc5c95b0dfeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module http

import net.http.chunked
import strconv

// Response represents the result of the request
pub struct Response {
pub mut:
	text         string
	header       Header
	status_code  int
	status_msg   string
	http_version string
}

fn (mut resp Response) free() {
	unsafe { resp.header.free() }
}

// Formats resp to bytes suitable for HTTP response transmission
pub fn (resp Response) bytes() []byte {
	// TODO: build []byte directly; this uses two allocations
	return resp.bytestr().bytes()
}

// Formats resp to a string suitable for HTTP response transmission
pub fn (resp Response) bytestr() string {
	return ('HTTP/$resp.http_version $resp.status_code $resp.status_msg\r\n' + '${resp.header.render(
		version: resp.version()
	)}\r\n' + '$resp.text')
}

// Parse a raw HTTP response into a Response object
pub fn parse_response(resp string) ?Response {
	version, status_code, status_msg := parse_status_line(resp.all_before('\n')) ?
	// Build resp header map and separate the body
	start_idx, end_idx := find_headers_range(resp) ?
	header := parse_headers(resp.substr(start_idx, end_idx)) ?
	mut text := resp.substr(end_idx, resp.len)
	if header.get(.transfer_encoding) or { '' } == 'chunked' {
		text = chunked.decode(text)
	}
	return Response{
		http_version: version
		status_code: status_code
		status_msg: status_msg
		header: header
		text: text
	}
}

// parse_status_line parses the first HTTP response line into the HTTP
// version, status code, and reason phrase
fn parse_status_line(line string) ?(string, int, string) {
	if line.len < 5 || line[..5].to_lower() != 'http/' {
		return error('response does not start with HTTP/')
	}
	data := line.split_nth(' ', 3)
	if data.len != 3 {
		return error('expected at least 3 tokens')
	}
	version := data[0].substr(5, data[0].len)
	// validate version is 1*DIGIT "." 1*DIGIT
	digits := version.split_nth('.', 3)
	if digits.len != 2 {
		return error('HTTP version malformed')
	}
	for digit in digits {
		strconv.atoi(digit) or { return error('HTTP version must contain only integers') }
	}
	return version, strconv.atoi(data[1]) ?, data[2]
}

// 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 {
	return version_from_str('HTTP/$r.http_version')
}

// 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()
	r.http_version = '${maj}.$min'
}

pub struct ResponseConfig {
	version Version = .v1_1
	status  Status  = .ok
	header  Header
	text    string
}

// new_response creates a Response object from the configuration. This
// function will add a Content-Length header if text is not empty.
pub fn new_response(conf ResponseConfig) Response {
	mut resp := Response{
		text: conf.text
		header: conf.header
	}
	if conf.text.len > 0 && !resp.header.contains(.content_length) {
		resp.header.add(.content_length, conf.text.len.str())
	}
	resp.set_status(conf.status)
	resp.set_version(conf.version)
	return resp
}

// 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).
fn find_headers_range(data string) ?(int, int) {
	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')
}