mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
net.http: change header behavior to keep custom header case (#9602)
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// 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
|
||||
@@ -323,11 +325,15 @@ const common_header_map = map{
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,80 +355,136 @@ pub fn new_header(kvs ...HeaderConfig) Header {
|
||||
|
||||
// Append a value to the header key.
|
||||
pub fn (mut h Header) add(key CommonHeader, value string) {
|
||||
h.data[key.str()] << value
|
||||
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.
|
||||
pub fn (mut h Header) add_str(key string, value string) ? {
|
||||
k := canonicalize(key) ?
|
||||
h.data[k] << value
|
||||
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
|
||||
// that exist for the CommonHeader.
|
||||
pub fn (mut h Header) set(key CommonHeader, value string) {
|
||||
h.data[key.str()] = [value]
|
||||
k := key.str()
|
||||
h.data[k] = [value]
|
||||
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 CommonHeader.
|
||||
pub fn (mut h Header) set_str(key string, value string) {
|
||||
k := canonicalize(key) or { return }
|
||||
h.data[k] = [value]
|
||||
// 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.
|
||||
pub fn (mut h Header) delete(key CommonHeader) {
|
||||
h.data.delete(key.str())
|
||||
h.delete_custom(key.str())
|
||||
}
|
||||
|
||||
// Delete all values for a custom header key.
|
||||
pub fn (mut h Header) delete_str(key string) {
|
||||
k := canonicalize(key) or { return }
|
||||
h.data.delete(k)
|
||||
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 data 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]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether the header key exists in the map.
|
||||
pub fn (h Header) contains(key CommonHeader) bool {
|
||||
return key.str() in h.data
|
||||
return h.contains_custom(key.str())
|
||||
}
|
||||
|
||||
pub struct HeaderQueryConfig {
|
||||
exact bool
|
||||
}
|
||||
|
||||
// Returns whether the custom header key exists in the map.
|
||||
pub fn (h Header) contains_str(key string) bool {
|
||||
k := canonicalize(key) or { return false }
|
||||
return k in h.data
|
||||
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
|
||||
}
|
||||
|
||||
// Gets the first value for the CommonHeader, or none if the key does
|
||||
// not exist.
|
||||
pub fn (h Header) get(key CommonHeader) ?string {
|
||||
k := key.str()
|
||||
if h.data[k].len == 0 {
|
||||
return none
|
||||
}
|
||||
return h.data[k][0]
|
||||
return h.get_custom(key.str())
|
||||
}
|
||||
|
||||
// Gets the first value for the custom header, or none if the key does
|
||||
// not exist.
|
||||
pub fn (h Header) get_str(key string) ?string {
|
||||
k := canonicalize(key) or { return none }
|
||||
if h.data[k].len == 0 {
|
||||
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[k][0]
|
||||
return h.data[data_key][0]
|
||||
}
|
||||
|
||||
// Gets all values for the CommonHeader.
|
||||
pub fn (h Header) values(key CommonHeader) []string {
|
||||
return h.data[key.str()]
|
||||
return h.custom_values(key.str())
|
||||
}
|
||||
|
||||
// Gets all values for the custom header.
|
||||
pub fn (h Header) values_str(key string) []string {
|
||||
k := canonicalize(key) or { return [] }
|
||||
return h.data[k]
|
||||
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
|
||||
}
|
||||
|
||||
// Gets all header keys as strings
|
||||
@@ -430,37 +492,87 @@ pub fn (h Header) keys() []string {
|
||||
return h.data.keys()
|
||||
}
|
||||
|
||||
// Validate and canonicalize an HTTP header key
|
||||
// A canonical header is all lowercase except for the first character
|
||||
// and any character after a `-`. Example: `Example-Header-Key`
|
||||
// There are some exceptions like `DNT`, `WWW-Authenticate`, etc. For these we
|
||||
// check if the lowercase matches any in the common_header_map and return that.
|
||||
fn canonicalize(s string) ?string {
|
||||
// check for valid header bytes
|
||||
for _, c in s {
|
||||
pub struct HeaderRenderConfig {
|
||||
version Version
|
||||
coerce bool
|
||||
canonicalize bool
|
||||
}
|
||||
|
||||
// Renders the Header into a string for use in sending
|
||||
// HTTP requests. All header lines will end in `\n\r`
|
||||
[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]
|
||||
}
|
||||
sb.write_string(key)
|
||||
sb.write_string(': ')
|
||||
for i in 0 .. data_keys.len - 1 {
|
||||
k := data_keys[i]
|
||||
for v in h.data[k] {
|
||||
sb.write_string(v)
|
||||
sb.write_string(',')
|
||||
}
|
||||
}
|
||||
k := data_keys[data_keys.len - 1]
|
||||
sb.write_string(h.data[k].join(','))
|
||||
sb.write_string('\n\r')
|
||||
}
|
||||
} else {
|
||||
for k, v in h.data {
|
||||
key := if flags.version == .v2_0 {
|
||||
k.to_lower()
|
||||
} else if flags.canonicalize {
|
||||
canonicalize(k.to_lower())
|
||||
} else {
|
||||
k
|
||||
}
|
||||
sb.write_string(key)
|
||||
sb.write_string(': ')
|
||||
sb.write_string(v.join(','))
|
||||
sb.write_string('\n\r')
|
||||
}
|
||||
}
|
||||
res := sb.str()
|
||||
unsafe { sb.free() }
|
||||
return res
|
||||
}
|
||||
|
||||
// Canonicalize 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
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the header token is valid
|
||||
fn is_valid(header string) ? {
|
||||
for _, c in header {
|
||||
if int(c) >= 128 || !is_token(c) {
|
||||
return error('Invalid header key')
|
||||
}
|
||||
}
|
||||
|
||||
// check if we have a common header
|
||||
sl := s.to_lower()
|
||||
if sl in http.common_header_map {
|
||||
return http.common_header_map[sl].str()
|
||||
}
|
||||
|
||||
// check for canonicalization; create a new string if not
|
||||
mut upper := true
|
||||
for _, c in s {
|
||||
if upper && `a` <= c && c <= `z` {
|
||||
return s.to_lower().split('-').map(it.capitalize()).join('-')
|
||||
}
|
||||
if !upper && `A` <= c && c <= `Z` {
|
||||
return s.to_lower().split('-').map(it.capitalize()).join('-')
|
||||
}
|
||||
upper = c == `-`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Checks if the byte is valid for a header token
|
||||
@@ -470,3 +582,9 @@ fn is_token(b byte) bool {
|
||||
else { false }
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user