diff options
Diffstat (limited to 'v_windows/v/vlib/net/http/header.v')
-rw-r--r-- | v_windows/v/vlib/net/http/header.v | 698 |
1 files changed, 698 insertions, 0 deletions
diff --git a/v_windows/v/vlib/net/http/header.v b/v_windows/v/vlib/net/http/header.v new file mode 100644 index 0000000..c05bdbc --- /dev/null +++ b/v_windows/v/vlib/net/http/header.v @@ -0,0 +1,698 @@ +// 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 strings + +// CommonHeader is an enum of the most common HTTP headers +pub enum CommonHeader { + accept + accept_ch + accept_charset + accept_ch_lifetime + accept_encoding + accept_language + accept_patch + accept_post + accept_ranges + access_control_allow_credentials + access_control_allow_headers + access_control_allow_methods + access_control_allow_origin + access_control_expose_headers + access_control_max_age + access_control_request_headers + access_control_request_method + age + allow + alt_svc + authorization + cache_control + clear_site_data + connection + content_disposition + content_encoding + content_language + content_length + content_location + content_range + content_security_policy + content_security_policy_report_only + content_type + cookie + cross_origin_embedder_policy + cross_origin_opener_policy + cross_origin_resource_policy + date + device_memory + digest + dnt + early_data + etag + expect + expect_ct + expires + feature_policy + forwarded + from + host + if_match + if_modified_since + if_none_match + if_range + if_unmodified_since + index + keep_alive + large_allocation + last_modified + link + location + nel + origin + pragma + proxy_authenticate + proxy_authorization + range + referer + referrer_policy + retry_after + save_data + sec_fetch_dest + sec_fetch_mode + sec_fetch_site + sec_fetch_user + sec_websocket_accept + server + server_timing + set_cookie + sourcemap + strict_transport_security + te + timing_allow_origin + tk + trailer + transfer_encoding + upgrade + upgrade_insecure_requests + user_agent + vary + via + want_digest + warning + www_authenticate + x_content_type_options + x_dns_prefetch_control + x_forwarded_for + x_forwarded_host + x_forwarded_proto + x_frame_options + x_xss_protection +} + +pub fn (h CommonHeader) str() string { + return match h { + .accept { 'Accept' } + .accept_ch { 'Accept-CH' } + .accept_charset { 'Accept-Charset' } + .accept_ch_lifetime { 'Accept-CH-Lifetime' } + .accept_encoding { 'Accept-Encoding' } + .accept_language { 'Accept-Language' } + .accept_patch { 'Accept-Patch' } + .accept_post { 'Accept-Post' } + .accept_ranges { 'Accept-Ranges' } + .access_control_allow_credentials { 'Access-Control-Allow-Credentials' } + .access_control_allow_headers { 'Access-Control-Allow-Headers' } + .access_control_allow_methods { 'Access-Control-Allow-Methods' } + .access_control_allow_origin { 'Access-Control-Allow-Origin' } + .access_control_expose_headers { 'Access-Control-Expose-Headers' } + .access_control_max_age { 'Access-Control-Max-Age' } + .access_control_request_headers { 'Access-Control-Request-Headers' } + .access_control_request_method { 'Access-Control-Request-Method' } + .age { 'Age' } + .allow { 'Allow' } + .alt_svc { 'Alt-Svc' } + .authorization { 'Authorization' } + .cache_control { 'Cache-Control' } + .clear_site_data { 'Clear-Site-Data' } + .connection { 'Connection' } + .content_disposition { 'Content-Disposition' } + .content_encoding { 'Content-Encoding' } + .content_language { 'Content-Language' } + .content_length { 'Content-Length' } + .content_location { 'Content-Location' } + .content_range { 'Content-Range' } + .content_security_policy { 'Content-Security-Policy' } + .content_security_policy_report_only { 'Content-Security-Policy-Report-Only' } + .content_type { 'Content-Type' } + .cookie { 'Cookie' } + .cross_origin_embedder_policy { 'Cross-Origin-Embedder-Policy' } + .cross_origin_opener_policy { 'Cross-Origin-Opener-Policy' } + .cross_origin_resource_policy { 'Cross-Origin-Resource-Policy' } + .date { 'Date' } + .device_memory { 'Device-Memory' } + .digest { 'Digest' } + .dnt { 'DNT' } + .early_data { 'Early-Data' } + .etag { 'ETag' } + .expect { 'Expect' } + .expect_ct { 'Expect-CT' } + .expires { 'Expires' } + .feature_policy { 'Feature-Policy' } + .forwarded { 'Forwarded' } + .from { 'From' } + .host { 'Host' } + .if_match { 'If-Match' } + .if_modified_since { 'If-Modified-Since' } + .if_none_match { 'If-None-Match' } + .if_range { 'If-Range' } + .if_unmodified_since { 'If-Unmodified-Since' } + .index { 'Index' } + .keep_alive { 'Keep-Alive' } + .large_allocation { 'Large-Allocation' } + .last_modified { 'Last-Modified' } + .link { 'Link' } + .location { 'Location' } + .nel { 'NEL' } + .origin { 'Origin' } + .pragma { 'Pragma' } + .proxy_authenticate { 'Proxy-Authenticate' } + .proxy_authorization { 'Proxy-Authorization' } + .range { 'Range' } + .referer { 'Referer' } + .referrer_policy { 'Referrer-Policy' } + .retry_after { 'Retry-After' } + .save_data { 'Save-Data' } + .sec_fetch_dest { 'Sec-Fetch-Dest' } + .sec_fetch_mode { 'Sec-Fetch-Mode' } + .sec_fetch_site { 'Sec-Fetch-Site' } + .sec_fetch_user { 'Sec-Fetch-User' } + .sec_websocket_accept { 'Sec-WebSocket-Accept' } + .server { 'Server' } + .server_timing { 'Server-Timing' } + .set_cookie { 'Set-Cookie' } + .sourcemap { 'SourceMap' } + .strict_transport_security { 'Strict-Transport-Security' } + .te { 'TE' } + .timing_allow_origin { 'Timing-Allow-Origin' } + .tk { 'Tk' } + .trailer { 'Trailer' } + .transfer_encoding { 'Transfer-Encoding' } + .upgrade { 'Upgrade' } + .upgrade_insecure_requests { 'Upgrade-Insecure-Requests' } + .user_agent { 'User-Agent' } + .vary { 'Vary' } + .via { 'Via' } + .want_digest { 'Want-Digest' } + .warning { 'Warning' } + .www_authenticate { 'WWW-Authenticate' } + .x_content_type_options { 'X-Content-Type-Options' } + .x_dns_prefetch_control { 'X-DNS-Prefetch-Control' } + .x_forwarded_for { 'X-Forwarded-For' } + .x_forwarded_host { 'X-Forwarded-Host' } + .x_forwarded_proto { 'X-Forwarded-Proto' } + .x_frame_options { 'X-Frame-Options' } + .x_xss_protection { 'X-XSS-Protection' } + } +} + +const common_header_map = { + 'accept': CommonHeader.accept + 'accept-ch': .accept_ch + 'accept-charset': .accept_charset + 'accept-ch-lifetime': .accept_ch_lifetime + 'accept-encoding': .accept_encoding + 'accept-language': .accept_language + 'accept-patch': .accept_patch + 'accept-post': .accept_post + 'accept-ranges': .accept_ranges + 'access-control-allow-credentials': .access_control_allow_credentials + 'access-control-allow-headers': .access_control_allow_headers + 'access-control-allow-methods': .access_control_allow_methods + 'access-control-allow-origin': .access_control_allow_origin + 'access-control-expose-headers': .access_control_expose_headers + 'access-control-max-age': .access_control_max_age + 'access-control-request-headers': .access_control_request_headers + 'access-control-request-method': .access_control_request_method + 'age': .age + 'allow': .allow + 'alt-svc': .alt_svc + 'authorization': .authorization + 'cache-control': .cache_control + 'clear-site-data': .clear_site_data + 'connection': .connection + 'content-disposition': .content_disposition + 'content-encoding': .content_encoding + 'content-language': .content_language + 'content-length': .content_length + 'content-location': .content_location + 'content-range': .content_range + 'content-security-policy': .content_security_policy + 'content-security-policy-report-only': .content_security_policy_report_only + 'content-type': .content_type + 'cookie': .cookie + 'cross-origin-embedder-policy': .cross_origin_embedder_policy + 'cross-origin-opener-policy': .cross_origin_opener_policy + 'cross-origin-resource-policy': .cross_origin_resource_policy + 'date': .date + 'device-memory': .device_memory + 'digest': .digest + 'dnt': .dnt + 'early-data': .early_data + 'etag': .etag + 'expect': .expect + 'expect-ct': .expect_ct + 'expires': .expires + 'feature-policy': .feature_policy + 'forwarded': .forwarded + 'from': .from + 'host': .host + 'if-match': .if_match + 'if-modified-since': .if_modified_since + 'if-none-match': .if_none_match + 'if-range': .if_range + 'if-unmodified-since': .if_unmodified_since + 'index': .index + 'keep-alive': .keep_alive + 'large-allocation': .large_allocation + 'last-modified': .last_modified + 'link': .link + 'location': .location + 'nel': .nel + 'origin': .origin + 'pragma': .pragma + 'proxy-authenticate': .proxy_authenticate + 'proxy-authorization': .proxy_authorization + 'range': .range + 'referer': .referer + 'referrer-policy': .referrer_policy + 'retry-after': .retry_after + 'save-data': .save_data + 'sec-fetch-dest': .sec_fetch_dest + 'sec-fetch-mode': .sec_fetch_mode + 'sec-fetch-site': .sec_fetch_site + 'sec-fetch-user': .sec_fetch_user + 'sec-websocket-accept': .sec_websocket_accept + 'server': .server + 'server-timing': .server_timing + 'set-cookie': .set_cookie + 'sourcemap': .sourcemap + 'strict-transport-security': .strict_transport_security + 'te': .te + 'timing-allow-origin': .timing_allow_origin + 'tk': .tk + 'trailer': .trailer + 'transfer-encoding': .transfer_encoding + 'upgrade': .upgrade + 'upgrade-insecure-requests': .upgrade_insecure_requests + 'user-agent': .user_agent + 'vary': .vary + 'via': .via + 'want-digest': .want_digest + 'warning': .warning + 'www-authenticate': .www_authenticate + 'x-content-type-options': .x_content_type_options + 'x-dns-prefetch-control': .x_dns_prefetch_control + 'x-forwarded-for': .x_forwarded_for + 'x-forwarded-host': .x_forwarded_host + 'x-forwarded-proto': .x_forwarded_proto + 'x-frame-options': .x_frame_options + 'x-xss-protection': .x_xss_protection +} + +// Header represents the key-value pairs in an HTTP header +[noinit] +pub struct Header { +mut: + data map[string][]string + // map of lowercase header keys to their original keys + // in order of appearance + keys map[string][]string +} + +pub fn (mut h Header) free() { + unsafe { + h.data.free() + h.keys.free() + } +} + +pub struct HeaderConfig { + key CommonHeader + value string +} + +// Create a new Header object +pub fn new_header(kvs ...HeaderConfig) Header { + mut h := Header{ + data: map[string][]string{} + } + for kv in kvs { + h.add(kv.key, kv.value) + } + return h +} + +// 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) +} + +// 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) +} + +// 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() + h.data[k] = [value] + h.add_key(k) +} + +// 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 deletes all values for a key. +pub fn (mut h Header) delete(key CommonHeader) { + h.delete_custom(key.str()) +} + +// delete_custom deletes all values for a custom header key. +pub fn (mut h Header) delete_custom(key string) { + h.data.delete(key) + + // remove key from keys metadata + kl := key.to_lower() + if kl in h.keys { + h.keys[kl] = h.keys[kl].filter(it != key) + } +} + +pub struct HeaderCoerceConfig { + canonicalize bool +} + +// 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) + + for kl, data_keys in h.keys { + master_key := if canon { canonicalize(kl) } else { data_keys[0] } + + // save master data + master_data := h.data[master_key] + h.data.delete(master_key) + + for key in data_keys { + if key == master_key { + h.data[master_key] << master_data + continue + } + h.data[master_key] << h.data[key] + h.data.delete(key) + } + h.keys[kl] = [master_key] + } +} + +// contains returns whether the header key exists in the map. +pub fn (h Header) contains(key CommonHeader) bool { + return h.contains_custom(key.str()) +} + +pub struct HeaderQueryConfig { + exact bool +} + +// 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 + } + return key.to_lower() in h.keys +} + +// 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()) +} + +// 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) { + // get the first key from key metadata + k := key.to_lower() + if h.keys[k].len == 0 { + return none + } + data_key = h.keys[k][0] + } + if h.data[data_key].len == 0 { + return none + } + return h.data[data_key][0] +} + +// 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) { + return k + } + } + return none +} + +// values gets all values for the CommonHeader. +pub fn (h Header) values(key CommonHeader) []string { + return h.custom_values(key.str()) +} + +// 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] + } + // case insensitive lookup + mut values := []string{cap: 10} + for k in h.keys[key.to_lower()] { + values << h.data[k] + } + return values +} + +// keys gets all header keys as strings +pub fn (h Header) keys() []string { + return h.data.keys() +} + +pub struct HeaderRenderConfig { + version Version + coerce bool + canonicalize bool +} + +// 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 + mut sb := strings.new_builder(h.data.len * 48) + if flags.coerce { + for kl, data_keys in h.keys { + key := if flags.version == .v2_0 { + kl + } else if flags.canonicalize { + canonicalize(kl) + } else { + data_keys[0] + } + for k in data_keys { + for v in h.data[k] { + sb.write_string(key) + sb.write_string(': ') + sb.write_string(v) + sb.write_string('\r\n') + } + } + } + } else { + for k, vs in h.data { + key := if flags.version == .v2_0 { + k.to_lower() + } else if flags.canonicalize { + canonicalize(k.to_lower()) + } else { + k + } + for v in vs { + sb.write_string(key) + sb.write_string(': ') + sb.write_string(v) + sb.write_string('\r\n') + } + } + } + res := sb.str() + unsafe { sb.free() } + return res +} + +// 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 +fn canonicalize(sl string) string { + // check if we have a common header + if sl in http.common_header_map { + return http.common_header_map[sl].str() + } + return sl.split('-').map(it.capitalize()).join('-') +} + +// Helper function to add a key to the keys map +fn (mut h Header) add_key(key string) { + kl := key.to_lower() + if !h.keys[kl].contains(key) { + h.keys[kl] << key + } +} + +// Custom error struct for invalid header tokens +struct HeaderKeyError { + msg string + code int + header string + invalid_char byte +} + +// 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) { + return IError(HeaderKeyError{ + msg: "Invalid header key: '$header'" + code: 1 + header: header + invalid_char: c + }) + } + } + if header.len == 0 { + return IError(HeaderKeyError{ + msg: "Invalid header key: '$header'" + code: 2 + header: header + invalid_char: 0 + }) + } +} + +// 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 } + else { false } + } +} + +// 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) +} + +// parse_headers parses a newline delimited string into a Header struct +fn parse_headers(s string) ?Header { + mut h := new_header() + mut last_key := '' + mut last_value := '' + for line in s.split_into_lines() { + if line.len == 0 { + break + } + // handle header fold + if line[0] == ` ` || line[0] == `\t` { + last_value += ' ${line.trim(' \t')}' + continue + } else if last_key != '' { + h.add_custom(last_key, last_value) ? + } + last_key, last_value = parse_header(line) ? + } + h.add_custom(last_key, last_value) ? + return h +} + +fn parse_header(s string) ?(string, string) { + if !s.contains(':') { + return error('missing colon in header') + } + words := s.split_nth(':', 2) + // TODO: parse quoted text according to the RFC + return words[0], words[1].trim(' \t') +} |