From 3039092b8974ee5e4c1250661a7ff96e10abe66e Mon Sep 17 00:00:00 2001 From: Miccah Date: Mon, 12 Jul 2021 04:16:41 -0500 Subject: [PATCH] net.http: add header_from_map and join functions (#10747) --- vlib/net/http/header.v | 102 ++++++++++++++++++++++++++---------- vlib/net/http/header_test.v | 49 +++++++++++++++++ vlib/vweb/vweb.v | 63 +++++++--------------- 3 files changed, 143 insertions(+), 71 deletions(-) diff --git a/vlib/net/http/header.v b/vlib/net/http/header.v index 923e7e7a40..e7804bc6d5 100644 --- a/vlib/net/http/header.v +++ b/vlib/net/http/header.v @@ -353,22 +353,50 @@ pub fn new_header(kvs ...HeaderConfig) Header { 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) { k := key.str() h.data[k] << value h.add_key(k) } -// Append a value to a custom header key. This function will return an error -// if the key contains invalid header characters. +// add_custom appends a value to a custom header key. This function will +// return an error if the key contains invalid header characters. pub fn (mut h Header) add_custom(key string, value string) ? { is_valid(key) ? h.data[key] << value 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. pub fn (mut h Header) set(key CommonHeader, value string) { k := key.str() @@ -376,21 +404,22 @@ pub fn (mut h Header) set(key CommonHeader, value string) { h.add_key(k) } -// Sets the key-value pair for a custom header key. This function will -// clear any other values that exist for the header. This function will -// return an error if the key contains invalid header characters. +// set_custom sets the key-value pair for a custom header key. This +// function will clear any other values that exist for the header. This +// function will return an error if the key contains invalid header +// characters. pub fn (mut h Header) set_custom(key string, value string) ? { is_valid(key) ? h.data[key] = [value] 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) { 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) { h.data.delete(key) @@ -405,7 +434,8 @@ pub struct HeaderCoerceConfig { 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) { 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 { return h.contains_custom(key.str()) } @@ -437,7 +467,7 @@ pub struct HeaderQueryConfig { 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 { if flags.any(it.exact) { 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 } -// Gets the first value for the CommonHeader, or none if the key does -// not exist. +// get gets the first value for the CommonHeader, or none if the key +// does not exist. pub fn (h Header) get(key CommonHeader) ?string { return h.get_custom(key.str()) } -// Gets the first value for the custom header, or none if the key does -// not exist. +// get_custom gets the first value for the custom header, or none if +// the key does not exist. pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string { mut data_key := key 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] } -// 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 { for k, _ in h.data { if k.starts_with(key) { @@ -479,12 +510,12 @@ pub fn (h Header) starting_with(key string) ?string { return none } -// Gets all values for the CommonHeader. +// values gets all values for the CommonHeader. pub fn (h Header) values(key CommonHeader) []string { 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 { if flags.any(it.exact) { return h.data[key] @@ -497,7 +528,7 @@ pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string return values } -// Gets all header keys as strings +// keys gets all header keys as strings pub fn (h Header) keys() []string { return h.data.keys() } @@ -508,8 +539,8 @@ pub struct HeaderRenderConfig { canonicalize bool } -// Renders the Header into a string for use in sending -// HTTP requests. All header lines will end in `\r\n` +// render renders the Header into a string for use in sending HTTP +// requests. All header lines will end in `\r\n` [manualfree] pub fn (h Header) render(flags HeaderRenderConfig) string { // estimate ~48 bytes per header @@ -556,7 +587,24 @@ pub fn (h Header) render(flags HeaderRenderConfig) string { 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 // 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 @@ -584,7 +632,7 @@ struct HeaderKeyError { 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) ? { for _, c in header { 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 { return match b { 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 -// Key order is not guaranteed +// str returns the headers string as seen in HTTP/1.1 requests. +// Key order is not guaranteed. pub fn (h Header) str() string { return h.render(version: .v1_1) } diff --git a/vlib/net/http/header_test.v b/vlib/net/http/header_test.v index 1b4eacb96d..573e58a0e2 100644 --- a/vlib/net/http/header_test.v +++ b/vlib/net/http/header_test.v @@ -274,3 +274,52 @@ fn test_str() ? { 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' } + +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') +} diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index 293b1682f8..dbe4315a9a 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -13,56 +13,37 @@ import time pub const ( 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{ version: .v1_1 status_code: 400 text: '400 Bad Request' - header: fn () http.Header { - mut h := http.new_header() - h.add(.content_type, 'text/plain') - h.add(.content_length, '15') - close := headers_close() - for k in close.keys() { - for v in close.custom_values(k) { - h.add_custom(k, v) or {} - } - } - return h - }() + header: http.new_header_from_map(map{ + http.CommonHeader.content_type: 'text/plain' + http.CommonHeader.content_length: '15' + }).join(headers_close) } http_404 = http.Response{ version: .v1_1 status_code: 404 text: '404 Not Found' - header: fn () http.Header { - mut h := http.new_header() - h.add(.content_type, 'text/plain') - h.add(.content_length, '13') - close := headers_close() - for k in close.keys() { - for v in close.custom_values(k) { - h.add_custom(k, v) or {} - } - } - return h - }() + header: http.new_header_from_map(map{ + http.CommonHeader.content_type: 'text/plain' + http.CommonHeader.content_length: '13' + }).join(headers_close) } http_500 = http.Response{ version: .v1_1 status_code: 500 text: '500 Internal Server Error' - header: fn () http.Header { - mut h := http.new_header() - h.add(.content_type, 'text/plain') - h.add(.content_length, '25') - close := headers_close() - for k in close.keys() { - for v in close.custom_values(k) { - h.add_custom(k, v) or {} - } - } - return h - }() + header: http.new_header_from_map(map{ + http.CommonHeader.content_type: 'text/plain' + http.CommonHeader.content_length: '25' + }).join(headers_close) } mime_types = map{ '.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('\r\n') - sb.write_string(headers_close().str()) + sb.write_string(vweb.headers_close.str()) sb.write_string('\r\n') if ctx.chunked_transfer { mut i := 0 @@ -236,7 +217,7 @@ pub fn (mut ctx Context) redirect(url string) Result { return Result{} } 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{} @@ -707,9 +688,3 @@ pub type RawHtml = string fn send_string(mut conn net.TcpConn, s string) ? { 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 -}