net.http: add header_from_map and join functions (#10747)

pull/10773/head
Miccah 2021-07-12 04:16:41 -05:00 committed by GitHub
parent 581280e6fc
commit 3039092b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 71 deletions

View File

@ -353,22 +353,50 @@ pub fn new_header(kvs ...HeaderConfig) Header {
return h return h
} }
// Append a value to the header key. // new_header_from_map creates a Header from key value pairs
pub fn new_header_from_map(kvs map[CommonHeader]string) Header {
mut h := new_header()
h.add_map(kvs)
return h
}
// new_custom_header_from_map creates a Header from string key value pairs
pub fn new_custom_header_from_map(kvs map[string]string) ?Header {
mut h := new_header()
h.add_custom_map(kvs) ?
return h
}
// add appends a value to the header key.
pub fn (mut h Header) add(key CommonHeader, value string) { pub fn (mut h Header) add(key CommonHeader, value string) {
k := key.str() k := key.str()
h.data[k] << value h.data[k] << value
h.add_key(k) h.add_key(k)
} }
// Append a value to a custom header key. This function will return an error // add_custom appends a value to a custom header key. This function will
// if the key contains invalid header characters. // return an error if the key contains invalid header characters.
pub fn (mut h Header) add_custom(key string, value string) ? { pub fn (mut h Header) add_custom(key string, value string) ? {
is_valid(key) ? is_valid(key) ?
h.data[key] << value h.data[key] << value
h.add_key(key) h.add_key(key)
} }
// Sets the key-value pair. This function will clear any other values // add_map appends the value for each header key.
pub fn (mut h Header) add_map(kvs map[CommonHeader]string) {
for k, v in kvs {
h.add(k, v)
}
}
// add_custom_map appends the value for each custom header key.
pub fn (mut h Header) add_custom_map(kvs map[string]string) ? {
for k, v in kvs {
h.add_custom(k, v) ?
}
}
// set sets the key-value pair. This function will clear any other values
// that exist for the CommonHeader. // that exist for the CommonHeader.
pub fn (mut h Header) set(key CommonHeader, value string) { pub fn (mut h Header) set(key CommonHeader, value string) {
k := key.str() k := key.str()
@ -376,21 +404,22 @@ pub fn (mut h Header) set(key CommonHeader, value string) {
h.add_key(k) h.add_key(k)
} }
// Sets the key-value pair for a custom header key. This function will // set_custom sets the key-value pair for a custom header key. This
// clear any other values that exist for the header. This function will // function will clear any other values that exist for the header. This
// return an error if the key contains invalid header characters. // function will return an error if the key contains invalid header
// characters.
pub fn (mut h Header) set_custom(key string, value string) ? { pub fn (mut h Header) set_custom(key string, value string) ? {
is_valid(key) ? is_valid(key) ?
h.data[key] = [value] h.data[key] = [value]
h.add_key(key) h.add_key(key)
} }
// Delete all values for a key. // delete deletes all values for a key.
pub fn (mut h Header) delete(key CommonHeader) { pub fn (mut h Header) delete(key CommonHeader) {
h.delete_custom(key.str()) h.delete_custom(key.str())
} }
// Delete all values for a custom header key. // delete_custom deletes all values for a custom header key.
pub fn (mut h Header) delete_custom(key string) { pub fn (mut h Header) delete_custom(key string) {
h.data.delete(key) h.data.delete(key)
@ -405,7 +434,8 @@ pub struct HeaderCoerceConfig {
canonicalize bool canonicalize bool
} }
// Coerce data by joining keys that match case-insensitively into one entry // coerce coerces data in the Header by joining keys that match
// case-insensitively into one entry.
pub fn (mut h Header) coerce(flags ...HeaderCoerceConfig) { pub fn (mut h Header) coerce(flags ...HeaderCoerceConfig) {
canon := flags.any(it.canonicalize) canon := flags.any(it.canonicalize)
@ -428,7 +458,7 @@ pub fn (mut h Header) coerce(flags ...HeaderCoerceConfig) {
} }
} }
// Returns whether the header key exists in the map. // contains returns whether the header key exists in the map.
pub fn (h Header) contains(key CommonHeader) bool { pub fn (h Header) contains(key CommonHeader) bool {
return h.contains_custom(key.str()) return h.contains_custom(key.str())
} }
@ -437,7 +467,7 @@ pub struct HeaderQueryConfig {
exact bool exact bool
} }
// Returns whether the custom header key exists in the map. // contains_custom returns whether the custom header key exists in the map.
pub fn (h Header) contains_custom(key string, flags ...HeaderQueryConfig) bool { pub fn (h Header) contains_custom(key string, flags ...HeaderQueryConfig) bool {
if flags.any(it.exact) { if flags.any(it.exact) {
return key in h.data return key in h.data
@ -445,14 +475,14 @@ pub fn (h Header) contains_custom(key string, flags ...HeaderQueryConfig) bool {
return key.to_lower() in h.keys return key.to_lower() in h.keys
} }
// Gets the first value for the CommonHeader, or none if the key does // get gets the first value for the CommonHeader, or none if the key
// not exist. // does not exist.
pub fn (h Header) get(key CommonHeader) ?string { pub fn (h Header) get(key CommonHeader) ?string {
return h.get_custom(key.str()) return h.get_custom(key.str())
} }
// Gets the first value for the custom header, or none if the key does // get_custom gets the first value for the custom header, or none if
// not exist. // the key does not exist.
pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string { pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string {
mut data_key := key mut data_key := key
if !flags.any(it.exact) { if !flags.any(it.exact) {
@ -469,7 +499,8 @@ pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string {
return h.data[data_key][0] return h.data[data_key][0]
} }
// Gets the first value of the header starting with key, or none if the key does not exist. // starting_with gets the first header starting with key, or none if
// the key does not exist.
pub fn (h Header) starting_with(key string) ?string { pub fn (h Header) starting_with(key string) ?string {
for k, _ in h.data { for k, _ in h.data {
if k.starts_with(key) { if k.starts_with(key) {
@ -479,12 +510,12 @@ pub fn (h Header) starting_with(key string) ?string {
return none return none
} }
// Gets all values for the CommonHeader. // values gets all values for the CommonHeader.
pub fn (h Header) values(key CommonHeader) []string { pub fn (h Header) values(key CommonHeader) []string {
return h.custom_values(key.str()) return h.custom_values(key.str())
} }
// Gets all values for the custom header. // custom_values gets all values for the custom header.
pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string { pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string {
if flags.any(it.exact) { if flags.any(it.exact) {
return h.data[key] return h.data[key]
@ -497,7 +528,7 @@ pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string
return values return values
} }
// Gets all header keys as strings // keys gets all header keys as strings
pub fn (h Header) keys() []string { pub fn (h Header) keys() []string {
return h.data.keys() return h.data.keys()
} }
@ -508,8 +539,8 @@ pub struct HeaderRenderConfig {
canonicalize bool canonicalize bool
} }
// Renders the Header into a string for use in sending // render renders the Header into a string for use in sending HTTP
// HTTP requests. All header lines will end in `\r\n` // requests. All header lines will end in `\r\n`
[manualfree] [manualfree]
pub fn (h Header) render(flags HeaderRenderConfig) string { pub fn (h Header) render(flags HeaderRenderConfig) string {
// estimate ~48 bytes per header // estimate ~48 bytes per header
@ -556,7 +587,24 @@ pub fn (h Header) render(flags HeaderRenderConfig) string {
return res return res
} }
// Canonicalize an HTTP header key // join combines two Header structs into a new Header struct
pub fn (h Header) join(other Header) Header {
mut combined := Header{
data: h.data.clone()
keys: h.keys.clone()
}
for k in other.keys() {
for v in other.custom_values(k, exact: true) {
combined.add_custom(k, v) or {
// panic because this should never fail
panic('unexpected error: $err')
}
}
}
return combined
}
// canonicalize canonicalizes an HTTP header key
// Common headers are determined by the common_header_map // Common headers are determined by the common_header_map
// Custom headers are capitalized on the first letter and any letter after a '-' // Custom headers are capitalized on the first letter and any letter after a '-'
// NOTE: Assumes sl is lowercase, since the caller usually already has the lowercase key // NOTE: Assumes sl is lowercase, since the caller usually already has the lowercase key
@ -584,7 +632,7 @@ struct HeaderKeyError {
invalid_char byte invalid_char byte
} }
// Checks if the header token is valid // is_valid checks if the header token contains all valid bytes
fn is_valid(header string) ? { fn is_valid(header string) ? {
for _, c in header { for _, c in header {
if int(c) >= 128 || !is_token(c) { if int(c) >= 128 || !is_token(c) {
@ -598,7 +646,7 @@ fn is_valid(header string) ? {
} }
} }
// Checks if the byte is valid for a header token // is_token checks if the byte is valid for a header token
fn is_token(b byte) bool { fn is_token(b byte) bool {
return match b { return match b {
33, 35...39, 42, 43, 45, 46, 48...57, 65...90, 94...122, 124, 126 { true } 33, 35...39, 42, 43, 45, 46, 48...57, 65...90, 94...122, 124, 126 { true }
@ -606,8 +654,8 @@ fn is_token(b byte) bool {
} }
} }
// Returns the headers string as seen in HTTP/1.1 requests // str returns the headers string as seen in HTTP/1.1 requests.
// Key order is not guaranteed // Key order is not guaranteed.
pub fn (h Header) str() string { pub fn (h Header) str() string {
return h.render(version: .v1_1) return h.render(version: .v1_1)
} }

View File

@ -274,3 +274,52 @@ fn test_str() ? {
assert h.str() == 'Accept: text/html,image/jpeg\r\nX-custom: Hello\r\n' assert h.str() == 'Accept: text/html,image/jpeg\r\nX-custom: Hello\r\n'
|| h.str() == 'X-custom: Hello\r\nAccept:text/html,image/jpeg\r\n' || h.str() == 'X-custom: Hello\r\nAccept:text/html,image/jpeg\r\n'
} }
fn test_header_from_map() ? {
h := new_header_from_map(map{
CommonHeader.accept: 'nothing'
CommonHeader.expires: 'yesterday'
})
assert h.contains(.accept)
assert h.contains(.expires)
assert h.get(.accept) or { '' } == 'nothing'
assert h.get(.expires) or { '' } == 'yesterday'
}
fn test_custom_header_from_map() ? {
h := new_custom_header_from_map(map{
'Server': 'VWeb'
'foo': 'bar'
}) ?
assert h.contains_custom('server')
assert h.contains_custom('foo')
assert h.get_custom('server') or { '' } == 'VWeb'
assert h.get_custom('foo') or { '' } == 'bar'
}
fn test_header_join() ? {
h1 := new_header_from_map(map{
CommonHeader.accept: 'nothing'
CommonHeader.expires: 'yesterday'
})
h2 := new_custom_header_from_map(map{
'Server': 'VWeb'
'foo': 'bar'
}) ?
h3 := h1.join(h2)
// h1 is unchanged
assert h1.contains(.accept)
assert h1.contains(.expires)
assert !h1.contains_custom('Server')
assert !h1.contains_custom('foo')
// h2 is unchanged
assert !h2.contains(.accept)
assert !h2.contains(.expires)
assert h2.contains_custom('Server')
assert h2.contains_custom('foo')
// h3 has all four headers
assert h3.contains(.accept)
assert h3.contains(.expires)
assert h3.contains_custom('Server')
assert h3.contains_custom('foo')
}

View File

@ -13,56 +13,37 @@ import time
pub const ( pub const (
methods_with_form = [http.Method.post, .put, .patch] methods_with_form = [http.Method.post, .put, .patch]
headers_close = http.new_custom_header_from_map(map{
'Server': 'VWeb'
http.CommonHeader.connection.str(): 'close'
}) or { panic('should never fail') }
http_400 = http.Response{ http_400 = http.Response{
version: .v1_1 version: .v1_1
status_code: 400 status_code: 400
text: '400 Bad Request' text: '400 Bad Request'
header: fn () http.Header { header: http.new_header_from_map(map{
mut h := http.new_header() http.CommonHeader.content_type: 'text/plain'
h.add(.content_type, 'text/plain') http.CommonHeader.content_length: '15'
h.add(.content_length, '15') }).join(headers_close)
close := headers_close()
for k in close.keys() {
for v in close.custom_values(k) {
h.add_custom(k, v) or {}
}
}
return h
}()
} }
http_404 = http.Response{ http_404 = http.Response{
version: .v1_1 version: .v1_1
status_code: 404 status_code: 404
text: '404 Not Found' text: '404 Not Found'
header: fn () http.Header { header: http.new_header_from_map(map{
mut h := http.new_header() http.CommonHeader.content_type: 'text/plain'
h.add(.content_type, 'text/plain') http.CommonHeader.content_length: '13'
h.add(.content_length, '13') }).join(headers_close)
close := headers_close()
for k in close.keys() {
for v in close.custom_values(k) {
h.add_custom(k, v) or {}
}
}
return h
}()
} }
http_500 = http.Response{ http_500 = http.Response{
version: .v1_1 version: .v1_1
status_code: 500 status_code: 500
text: '500 Internal Server Error' text: '500 Internal Server Error'
header: fn () http.Header { header: http.new_header_from_map(map{
mut h := http.new_header() http.CommonHeader.content_type: 'text/plain'
h.add(.content_type, 'text/plain') http.CommonHeader.content_length: '25'
h.add(.content_length, '25') }).join(headers_close)
close := headers_close()
for k in close.keys() {
for v in close.custom_values(k) {
h.add_custom(k, v) or {}
}
}
return h
}()
} }
mime_types = map{ mime_types = map{
'.css': 'text/css; charset=utf-8' '.css': 'text/css; charset=utf-8'
@ -161,7 +142,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo
} }
sb.write_string(ctx.headers) sb.write_string(ctx.headers)
sb.write_string('\r\n') sb.write_string('\r\n')
sb.write_string(headers_close().str()) sb.write_string(vweb.headers_close.str())
sb.write_string('\r\n') sb.write_string('\r\n')
if ctx.chunked_transfer { if ctx.chunked_transfer {
mut i := 0 mut i := 0
@ -236,7 +217,7 @@ pub fn (mut ctx Context) redirect(url string) Result {
return Result{} return Result{}
} }
ctx.done = true ctx.done = true
send_string(mut ctx.conn, 'HTTP/1.1 302 Found\r\nLocation: $url$ctx.headers\r\n$headers_close().str()\r\n') or { send_string(mut ctx.conn, 'HTTP/1.1 302 Found\r\nLocation: $url$ctx.headers\r\n$vweb.headers_close\r\n') or {
return Result{} return Result{}
} }
return Result{} return Result{}
@ -707,9 +688,3 @@ pub type RawHtml = string
fn send_string(mut conn net.TcpConn, s string) ? { fn send_string(mut conn net.TcpConn, s string) ? {
conn.write(s.bytes()) ? conn.write(s.bytes()) ?
} }
fn headers_close() http.Header {
mut h := http.new_header(key: .connection, value: 'close')
h.add_custom('Server', 'VWeb') or {}
return h
}