diff options
Diffstat (limited to 'v_windows/v/old/vlib/net/http')
21 files changed, 3627 insertions, 0 deletions
| diff --git a/v_windows/v/old/vlib/net/http/backend_nix.c.v b/v_windows/v/old/vlib/net/http/backend_nix.c.v new file mode 100644 index 0000000..1243442 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/backend_nix.c.v @@ -0,0 +1,74 @@ +// 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 +import net.openssl + +const ( +	is_used = openssl.is_used +) + +fn (req &Request) ssl_do(port int, method Method, host_name string, path string) ?Response { +	// ssl_method := C.SSLv23_method() +	ctx := C.SSL_CTX_new(C.TLS_method()) +	C.SSL_CTX_set_verify_depth(ctx, 4) +	flags := C.SSL_OP_NO_SSLv2 | C.SSL_OP_NO_SSLv3 | C.SSL_OP_NO_COMPRESSION +	C.SSL_CTX_set_options(ctx, flags) +	mut res := C.SSL_CTX_load_verify_locations(ctx, c'random-org-chain.pem', 0) +	web := C.BIO_new_ssl_connect(ctx) +	addr := host_name + ':' + port.str() +	res = C.BIO_set_conn_hostname(web, addr.str) +	ssl := &openssl.SSL(0) +	C.BIO_get_ssl(web, &ssl) +	preferred_ciphers := 'HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4' +	res = C.SSL_set_cipher_list(voidptr(ssl), &char(preferred_ciphers.str)) +	if res != 1 { +		println('http: openssl: cipher failed') +	} +	res = C.SSL_set_tlsext_host_name(voidptr(ssl), host_name.str) +	res = C.BIO_do_connect(web) +	if res != 1 { +		return error('cannot connect the endpoint') +	} +	res = C.BIO_do_handshake(web) +	C.SSL_get_peer_certificate(voidptr(ssl)) +	res = C.SSL_get_verify_result(voidptr(ssl)) +	// ///// +	req_headers := req.build_request_headers(method, host_name, path) +	$if trace_http_request ? { +		eprintln('> $req_headers') +	} +	// println(req_headers) +	C.BIO_puts(web, &char(req_headers.str)) +	mut content := strings.new_builder(100) +	mut buff := [bufsize]byte{} +	bp := unsafe { &buff[0] } +	mut readcounter := 0 +	for { +		readcounter++ +		len := unsafe { C.BIO_read(web, bp, bufsize) } +		if len <= 0 { +			break +		} +		$if debug_http ? { +			eprintln('ssl_do, read ${readcounter:4d} | len: $len') +			eprintln('-'.repeat(20)) +			eprintln(unsafe { tos(bp, len) }) +			eprintln('-'.repeat(20)) +		} +		unsafe { content.write_ptr(bp, len) } +	} +	if web != 0 { +		C.BIO_free_all(web) +	} +	if ctx != 0 { +		C.SSL_CTX_free(ctx) +	} +	response_text := content.str() +	$if trace_http_response ? { +		eprintln('< $response_text') +	} +	return parse_response(response_text) +} diff --git a/v_windows/v/old/vlib/net/http/backend_windows.c.v b/v_windows/v/old/vlib/net/http/backend_windows.c.v new file mode 100644 index 0000000..9181166 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/backend_windows.c.v @@ -0,0 +1,28 @@ +// 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 + +#flag windows -I @VEXEROOT/thirdparty/vschannel +#flag -l ws2_32 -l crypt32 -l secur32 -l user32 +#include "vschannel.c" + +fn C.new_tls_context() C.TlsContext + +fn (req &Request) ssl_do(port int, method Method, host_name string, path string) ?Response { +	mut ctx := C.new_tls_context() +	C.vschannel_init(&ctx) +	mut buff := unsafe { malloc_noscan(C.vsc_init_resp_buff_size) } +	addr := host_name +	sdata := req.build_request_headers(method, host_name, path) +	$if trace_http_request ? { +		eprintln('> $sdata') +	} +	length := C.request(&ctx, port, addr.to_wide(), sdata.str, &buff) +	C.vschannel_cleanup(&ctx) +	response_text := unsafe { buff.vstring_with_len(length) } +	$if trace_http_response ? { +		eprintln('< $response_text') +	} +	return parse_response(response_text) +} diff --git a/v_windows/v/old/vlib/net/http/chunked/dechunk.v b/v_windows/v/old/vlib/net/http/chunked/dechunk.v new file mode 100644 index 0000000..0e82586 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/chunked/dechunk.v @@ -0,0 +1,72 @@ +module chunked + +import strings +// See: https://en.wikipedia.org/wiki/Chunked_transfer_encoding +// ///////////////////////////////////////////////////////////// +// The chunk size is transferred as a hexadecimal number +// followed by \r\n as a line separator, +// followed by a chunk of data of the given size. +// The end is marked with a chunk with size 0. + +struct ChunkScanner { +mut: +	pos  int +	text string +} + +fn (mut s ChunkScanner) read_chunk_size() int { +	mut n := 0 +	for { +		if s.pos >= s.text.len { +			break +		} +		c := s.text[s.pos] +		if !c.is_hex_digit() { +			break +		} +		n = n << 4 +		n += int(unhex(c)) +		s.pos++ +	} +	return n +} + +fn unhex(c byte) byte { +	if `0` <= c && c <= `9` { +		return c - `0` +	} else if `a` <= c && c <= `f` { +		return c - `a` + 10 +	} else if `A` <= c && c <= `F` { +		return c - `A` + 10 +	} +	return 0 +} + +fn (mut s ChunkScanner) skip_crlf() { +	s.pos += 2 +} + +fn (mut s ChunkScanner) read_chunk(chunksize int) string { +	startpos := s.pos +	s.pos += chunksize +	return s.text[startpos..s.pos] +} + +pub fn decode(text string) string { +	mut sb := strings.new_builder(100) +	mut cscanner := ChunkScanner{ +		pos: 0 +		text: text +	} +	for { +		csize := cscanner.read_chunk_size() +		if 0 == csize { +			break +		} +		cscanner.skip_crlf() +		sb.write_string(cscanner.read_chunk(csize)) +		cscanner.skip_crlf() +	} +	cscanner.skip_crlf() +	return sb.str() +} diff --git a/v_windows/v/old/vlib/net/http/cookie.v b/v_windows/v/old/vlib/net/http/cookie.v new file mode 100644 index 0000000..d647b3d --- /dev/null +++ b/v_windows/v/old/vlib/net/http/cookie.v @@ -0,0 +1,413 @@ +// Copyright (c) 2019 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 time +import strings + +pub struct Cookie { +pub mut: +	name        string +	value       string +	path        string    // optional +	domain      string    // optional +	expires     time.Time // optional +	raw_expires string    // for reading cookies only. optional. +	// max_age=0 means no 'Max-Age' attribute specified. +	// max_age<0 means delete cookie now, equivalently 'Max-Age: 0' +	// max_age>0 means Max-Age attribute present and given in seconds +	max_age   int +	secure    bool +	http_only bool +	same_site SameSite +	raw       string +	unparsed  []string // Raw text of unparsed attribute-value pairs +} + +// SameSite allows a server to define a cookie attribute making it impossible for +// the browser to send this cookie along with cross-site requests. The main +// goal is to mitigate the risk of cross-origin information leakage, and provide +// some protection against cross-site request forgery attacks. +// +// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. +pub enum SameSite { +	same_site_default_mode = 1 +	same_site_lax_mode +	same_site_strict_mode +	same_site_none_mode +} + +// Parses all "Set-Cookie" values from the header `h` and +// returns the successfully parsed Cookies. +pub fn read_set_cookies(h map[string][]string) []&Cookie { +	cookies_s := h['Set-Cookie'] +	cookie_count := cookies_s.len +	if cookie_count == 0 { +		return [] +	} +	mut cookies := []&Cookie{} +	for _, line in cookies_s { +		c := parse_cookie(line) or { continue } +		cookies << &c +	} +	return cookies +} + +// Parses all "Cookie" values from the header `h` and +// returns the successfully parsed Cookies. +// +// if `filter` isn't empty, only cookies of that name are returned +pub fn read_cookies(h map[string][]string, filter string) []&Cookie { +	lines := h['Cookie'] +	if lines.len == 0 { +		return [] +	} +	mut cookies := []&Cookie{} +	for _, line_ in lines { +		mut line := line_.trim_space() +		mut part := '' +		for line.len > 0 { +			if line.index_any(';') > 0 { +				line_parts := line.split(';') +				part = line_parts[0] +				line = line_parts[1] +			} else { +				part = line +				line = '' +			} +			part = part.trim_space() +			if part.len == 0 { +				continue +			} +			mut name := part +			mut val := '' +			if part.contains('=') { +				val_parts := part.split('=') +				name = val_parts[0] +				val = val_parts[1] +			} +			if !is_cookie_name_valid(name) { +				continue +			} +			if filter != '' && filter != name { +				continue +			} +			val = parse_cookie_value(val, true) or { continue } +			cookies << &Cookie{ +				name: name +				value: val +			} +		} +	} +	return cookies +} + +// Returns the serialization of the cookie for use in a Cookie header +// (if only Name and Value are set) or a Set-Cookie response +// header (if other fields are set). +// +// If c.name is invalid, the empty string is returned. +pub fn (c &Cookie) str() string { +	if !is_cookie_name_valid(c.name) { +		return '' +	} +	// extra_cookie_length derived from typical length of cookie attributes +	// see RFC 6265 Sec 4.1. +	extra_cookie_length := 110 +	mut b := strings.new_builder(c.name.len + c.value.len + c.domain.len + c.path.len + +		extra_cookie_length) +	b.write_string(c.name) +	b.write_string('=') +	b.write_string(sanitize_cookie_value(c.value)) +	if c.path.len > 0 { +		b.write_string('; path=') +		b.write_string(sanitize_cookie_path(c.path)) +	} +	if c.domain.len > 0 { +		if valid_cookie_domain(c.domain) { +			// A `domain` containing illegal characters is not +			// sanitized but simply dropped which turns the cookie +			// into a host-only cookie. A leading dot is okay +			// but won't be sent. +			mut d := c.domain +			if d[0] == `.` { +				d = d.substr(1, d.len) +			} +			b.write_string('; domain=') +			b.write_string(d) +		} else { +			// TODO: Log invalid cookie domain warning +		} +	} +	if c.expires.year > 1600 { +		e := c.expires +		time_str := '$e.weekday_str(), $e.day.str() $e.smonth() $e.year $e.hhmmss() GMT' +		b.write_string('; expires=') +		b.write_string(time_str) +	} +	// TODO: Fix this. Techically a max age of 0 or less should be 0 +	// We need a way to not have a max age. +	if c.max_age > 0 { +		b.write_string('; Max-Age=') +		b.write_string(c.max_age.str()) +	} else if c.max_age < 0 { +		b.write_string('; Max-Age=0') +	} +	if c.http_only { +		b.write_string('; HttpOnly') +	} +	if c.secure { +		b.write_string('; Secure') +	} +	match c.same_site { +		.same_site_default_mode { +			b.write_string('; SameSite') +		} +		.same_site_none_mode { +			b.write_string('; SameSite=None') +		} +		.same_site_lax_mode { +			b.write_string('; SameSite=Lax') +		} +		.same_site_strict_mode { +			b.write_string('; SameSite=Strict') +		} +	} +	return b.str() +} + +fn sanitize(valid fn (byte) bool, v string) string { +	mut ok := true +	for i in 0 .. v.len { +		if valid(v[i]) { +			continue +		} +		// TODO: Warn that we're dropping the invalid byte? +		ok = false +		break +	} +	if ok { +		return v.clone() +	} +	return v.bytes().filter(valid(it)).bytestr() +} + +fn sanitize_cookie_name(name string) string { +	return name.replace_each(['\n', '-', '\r', '-']) +} + +// https://tools.ietf.org/html/rfc6265#section-4.1.1 +// cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) +// cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E +//           ; US-ASCII characters excluding CTLs, +//           ; whitespace DQUOTE, comma, semicolon, +//           ; and backslash +// We loosen this as spaces and commas are common in cookie values +// but we produce a quoted cookie-value in when value starts or ends +// with a comma or space. +pub fn sanitize_cookie_value(v string) string { +	val := sanitize(valid_cookie_value_byte, v) +	if v.len == 0 { +		return v +	} +	// Check for the existence of a space or comma +	if val.starts_with(' ') || val.ends_with(' ') || val.starts_with(',') || val.ends_with(',') { +		return '"$v"' +	} +	return v +} + +fn sanitize_cookie_path(v string) string { +	return sanitize(valid_cookie_path_byte, v) +} + +fn valid_cookie_value_byte(b byte) bool { +	return 0x20 <= b && b < 0x7f && b != `"` && b != `;` && b != `\\` +} + +fn valid_cookie_path_byte(b byte) bool { +	return 0x20 <= b && b < 0x7f && b != `!` +} + +fn valid_cookie_domain(v string) bool { +	if is_cookie_domain_name(v) { +		return true +	} +	// TODO +	// valid_ip := net.parse_ip(v) or { +	// 	false +	// } +	// if valid_ip { +	// 	return true +	// } +	return false +} + +pub fn is_cookie_domain_name(_s string) bool { +	mut s := _s +	if s.len == 0 { +		return false +	} +	if s.len > 255 { +		return false +	} +	if s[0] == `.` { +		s = s.substr(1, s.len) +	} +	mut last := `.` +	mut ok := false +	mut part_len := 0 +	for i, _ in s { +		c := s[i] +		if (`a` <= c && c <= `z`) || (`A` <= c && c <= `Z`) { +			// No '_' allowed here (in contrast to package net). +			ok = true +			part_len++ +		} else if `0` <= c && c <= `9` { +			// fine +			part_len++ +		} else if c == `-` { +			// Byte before dash cannot be dot. +			if last == `.` { +				return false +			} +			part_len++ +		} else if c == `.` { +			// Byte before dot cannot be dot, dash. +			if last == `.` || last == `-` { +				return false +			} +			if part_len > 63 || part_len == 0 { +				return false +			} +			part_len = 0 +		} else { +			return false +		} +		last = c +	} +	if last == `-` || part_len > 63 { +		return false +	} +	return ok +} + +fn parse_cookie_value(_raw string, allow_double_quote bool) ?string { +	mut raw := _raw +	// Strip the quotes, if present +	if allow_double_quote && raw.len > 1 && raw[0] == `"` && raw[raw.len - 1] == `"` { +		raw = raw.substr(1, raw.len - 1) +	} +	for i in 0 .. raw.len { +		if !valid_cookie_value_byte(raw[i]) { +			return error('http.cookie: invalid cookie value') +		} +	} +	return raw +} + +fn is_cookie_name_valid(name string) bool { +	if name == '' { +		return false +	} +	for b in name { +		if b < 33 || b > 126 { +			return false +		} +	} +	return true +} + +fn parse_cookie(line string) ?Cookie { +	mut parts := line.trim_space().split(';') +	if parts.len == 1 && parts[0] == '' { +		return error('malformed cookie') +	} +	parts[0] = parts[0].trim_space() +	keyval := parts[0].split('=') +	if keyval.len != 2 { +		return error('malformed cookie') +	} +	name := keyval[0] +	raw_value := keyval[1] +	if !is_cookie_name_valid(name) { +		return error('malformed cookie') +	} +	value := parse_cookie_value(raw_value, true) or { return error('malformed cookie') } +	mut c := Cookie{ +		name: name +		value: value +		raw: line +	} +	for i, _ in parts { +		parts[i] = parts[i].trim_space() +		if parts[i].len == 0 { +			continue +		} +		mut attr := parts[i] +		mut raw_val := '' +		if attr.contains('=') { +			pieces := attr.split('=') +			attr = pieces[0] +			raw_val = pieces[1] +		} +		lower_attr := attr.to_lower() +		val := parse_cookie_value(raw_val, false) or { +			c.unparsed << parts[i] +			continue +		} +		match lower_attr { +			'samesite' { +				lower_val := val.to_lower() +				match lower_val { +					'lax' { c.same_site = .same_site_lax_mode } +					'strict' { c.same_site = .same_site_strict_mode } +					'none' { c.same_site = .same_site_none_mode } +					else { c.same_site = .same_site_default_mode } +				} +			} +			'secure' { +				c.secure = true +				continue +			} +			'httponly' { +				c.http_only = true +				continue +			} +			'domain' { +				c.domain = val +				continue +			} +			'max-age' { +				mut secs := val.int() +				if secs != 0 && val[0] != `0` { +					break +				} +				if secs <= 0 { +					secs = -1 +				} +				c.max_age = secs +				continue +			} +			// TODO: Fix this once time works better +			// 'expires' { +			// 	c.raw_expires = val +			// 	mut exptime := time.parse_iso(val) +			// 	if exptime.year == 0 { +			// 		exptime = time.parse_iso('Mon, 02-Jan-2006 15:04:05 MST') +			// 	} +			// 	c.expires = exptime +			// 	continue +			// } +			'path' { +				c.path = val +				continue +			} +			else { +				c.unparsed << parts[i] +			} +		} +	} +	return c +} diff --git a/v_windows/v/old/vlib/net/http/cookie_test.v b/v_windows/v/old/vlib/net/http/cookie_test.v new file mode 100644 index 0000000..3806618 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/cookie_test.v @@ -0,0 +1,468 @@ +import net.http + +struct SetCookieTestCase { +	cookie &http.Cookie +	raw    string +} + +struct ReadSetCookiesTestCase { +	header  map[string][]string +	cookies []&http.Cookie +} + +struct AddCookieTestCase { +	cookie []&http.Cookie +	raw    string +} + +const ( +	write_set_cookie_tests = [ +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-1' +				value: 'v1' +			} +			raw: 'cookie-1=v1' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-2' +				value: 'two' +				max_age: 3600 +			} +			raw: 'cookie-2=two; Max-Age=3600' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-3' +				value: 'three' +				domain: '.example.com' +			} +			raw: 'cookie-3=three; domain=example.com' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-4' +				value: 'four' +				path: '/restricted/' +			} +			raw: 'cookie-4=four; path=/restricted/' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-5' +				value: 'five' +				domain: 'wrong;bad.abc' +			} +			raw: 'cookie-5=five' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-6' +				value: 'six' +				domain: 'bad-.abc' +			} +			raw: 'cookie-6=six' +		}, +		// SetCookieTestCase{ +		// 	cookie: &http.Cookie{name: 'cookie-7', value: 'seven', domain: '127.0.0.1'}, +		// 	raw: 'cookie-7=seven; domain=127.0.0.1' +		// }, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-8' +				value: 'eight' +				domain: '::1' +			} +			raw: 'cookie-8=eight' +		}, +		// { +		// 	cookie: &http.Cookie{name: 'cookie-9', value: 'expiring', expires: time.unix(1257894000, 0)}, +		// 	'cookie-9=expiring; Expires=Tue, 10 Nov 2009 23:00:00 GMT', +		// }, +		// According to IETF 6265 Section 5.1.1.5, the year cannot be less than 1601 +		// SetCookieTestCase{ +		// 	cookie: &http.Cookie{name: 'cookie-10', value: 'expiring-1601', expires: time.parse('Mon, 01 Jan 1601 01:01:01 GMT')}, +		// 	raw: 'cookie-10=expiring-1601; Expires=Mon, 01 Jan 1601 01:01:01 GMT' +		// }, +		// SetCookieTestCase{ +		// 	cookie: &http.Cookie{name: 'cookie-11', value: 'invalid-expiry', expires: time.parse('Mon, 01 Jan 1600 01:01:01 GMT')}, +		// 	raw: 'cookie-11=invalid-expiry' +		// }, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-12' +				value: 'samesite-default' +				same_site: .same_site_default_mode +			} +			raw: 'cookie-12=samesite-default; SameSite' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-13' +				value: 'samesite-lax' +				same_site: .same_site_lax_mode +			} +			raw: 'cookie-13=samesite-lax; SameSite=Lax' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-14' +				value: 'samesite-strict' +				same_site: .same_site_strict_mode +			} +			raw: 'cookie-14=samesite-strict; SameSite=Strict' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'cookie-15' +				value: 'samesite-none' +				same_site: .same_site_none_mode +			} +			raw: 'cookie-15=samesite-none; SameSite=None' +		}, +		// The 'special' cookies have values containing commas or spaces which +		// are disallowed by RFC 6265 but are common in the wild. +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-1' +				value: 'a z' +			} +			raw: 'special-1=a z' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-2' +				value: ' z' +			} +			raw: 'special-2=" z"' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-3' +				value: 'a ' +			} +			raw: 'special-3="a "' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-4' +				value: ' ' +			} +			raw: 'special-4=" "' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-5' +				value: 'a,z' +			} +			raw: 'special-5=a,z' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-6' +				value: ',z' +			} +			raw: 'special-6=",z"' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-7' +				value: 'a,' +			} +			raw: 'special-7="a,"' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'special-8' +				value: ',' +			} +			raw: 'special-8=","' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'empty-value' +				value: '' +			} +			raw: 'empty-value=' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: '' +			} +			raw: '' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: '\t' +			} +			raw: '' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: '\r' +			} +			raw: '' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'a\nb' +				value: 'v' +			} +			raw: '' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'a\nb' +				value: 'v' +			} +			raw: '' +		}, +		SetCookieTestCase{ +			cookie: &http.Cookie{ +				name: 'a\rb' +				value: 'v' +			} +			raw: '' +		}, +	] +	add_cookies_tests = [ +		AddCookieTestCase{ +			cookie: [] +			raw: '' +		}, +		AddCookieTestCase{ +			cookie: [&http.Cookie{ +				name: 'cookie-1' +				value: 'v1' +			}] +			raw: 'cookie-1=v1' +		}, +		AddCookieTestCase{ +			cookie: [&http.Cookie{ +				name: 'cookie-1' +				value: 'v1' +			}, +				&http.Cookie{ +				name: 'cookie-2' +				value: 'v2' +			}, +				&http.Cookie{ +					name: 'cookie-3' +					value: 'v3' +				}, +			] +			raw: 'cookie-1=v1; cookie-2=v2; cookie-3=v3' +		}, +	] +	read_set_cookies_tests = [ +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['Cookie-1=v1'] +			} +			cookies: [&http.Cookie{ +				name: 'Cookie-1' +				value: 'v1' +				raw: 'Cookie-1=v1' +			}] +		}, +		// ReadSetCookiesTestCase{ +		// 	header: {"Set-Cookie": ["NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"]}, +		// 	cookies: [&http.Cookie{ +		// 		name:       "NID", +		// 		value:      "99=YsDT5i3E-CXax-", +		// 		path:       "/", +		// 		domain:     ".google.ch", +		// 		http_only:   true, +		// 		expires:    time.parse_iso('Wed, 23-Nov-2011 01:05:03 GMT'), +		// 		raw_expires: "Wed, 23-Nov-2011 01:05:03 GMT", +		// 		raw:        "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly" +		// 	}] +		// }, +		// ReadSetCookiesTestCase{ +		// 	header: {"Set-Cookie": [".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"]}, +		// 	cookies: [&http.Cookie{ +		// 		name:       ".ASPXAUTH", +		// 		value:      "7E3AA", +		// 		path:       "/", +		// 		expires:    time.parse_iso('Wed, 07-Mar-2012 14:25:06 GMT'), +		// 		raw_expires: "Wed, 07-Mar-2012 14:25:06 GMT", +		// 		http_only:   true, +		// 		raw:        ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly" +		// 	}] +		// }, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['ASP.NET_SessionId=foo; path=/; HttpOnly'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'ASP.NET_SessionId' +				value: 'foo' +				path: '/' +				http_only: true +				raw: 'ASP.NET_SessionId=foo; path=/; HttpOnly' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['samesitedefault=foo; SameSite'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'samesitedefault' +				value: 'foo' +				same_site: .same_site_default_mode +				raw: 'samesitedefault=foo; SameSite' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['samesitelax=foo; SameSite=Lax'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'samesitelax' +				value: 'foo' +				same_site: .same_site_lax_mode +				raw: 'samesitelax=foo; SameSite=Lax' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['samesitestrict=foo; SameSite=Strict'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'samesitestrict' +				value: 'foo' +				same_site: .same_site_strict_mode +				raw: 'samesitestrict=foo; SameSite=Strict' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['samesitenone=foo; SameSite=None'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'samesitenone' +				value: 'foo' +				same_site: .same_site_none_mode +				raw: 'samesitenone=foo; SameSite=None' +			}, +			] +		}, +		// Make sure we can properly read back the Set-Cookie headers we create +		// for values containing spaces or commas: +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-1=a z'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-1' +				value: 'a z' +				raw: 'special-1=a z' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-2=" z"'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-2' +				value: ' z' +				raw: 'special-2=" z"' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-3="a "'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-3' +				value: 'a ' +				raw: 'special-3="a "' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-4=" "'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-4' +				value: ' ' +				raw: 'special-4=" "' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-5=a,z'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-5' +				value: 'a,z' +				raw: 'special-5=a,z' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-6=",z"'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-6' +				value: ',z' +				raw: 'special-6=",z"' +			}, +			] +		}, +		ReadSetCookiesTestCase{ +			header: map{ +				'Set-Cookie': ['special-7=","'] +			} +			cookies: [ +				&http.Cookie{ +				name: 'special-7' +				value: ',' +				raw: 'special-8=","' +			}, +			] +		} +		// TODO(bradfitz): users have reported seeing this in the +		// wild, but do browsers handle it? RFC 6265 just says "don't +		// do that" (section 3) and then never mentions header folding +		// again. +		// Header{"Set-Cookie": ["ASP.NET_SessionId=foo; path=/; HttpOnly, .ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"]}, +	] +) + +fn test_write_set_cookies() { +	for _, tt in write_set_cookie_tests { +		assert tt.cookie.str() == tt.raw +	} +} + +fn test_read_set_cookies() { +	for _, tt in read_set_cookies_tests { +		h := tt.header['Set-Cookie'][0] +		c := http.read_set_cookies(tt.header) +		println(h) +		println(c[0].str()) +		assert c[0].str() == h +	} +} diff --git a/v_windows/v/old/vlib/net/http/download.v b/v_windows/v/old/vlib/net/http/download.v new file mode 100644 index 0000000..455c1e0 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/download.v @@ -0,0 +1,18 @@ +// 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 os + +pub fn download_file(url string, out string) ? { +	$if debug_http ? { +		println('download file url=$url out=$out') +	} +	s := get(url) or { return err } +	if s.status() != .ok { +		return error('received http code $s.status_code') +	} +	os.write_file(out, s.text) ? +	// download_file_with_progress(url, out, empty, empty) +} diff --git a/v_windows/v/old/vlib/net/http/download_nix.c.v b/v_windows/v/old/vlib/net/http/download_nix.c.v new file mode 100644 index 0000000..724a256 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/download_nix.c.v @@ -0,0 +1,52 @@ +// 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 + +type DownloadFn = fn (written int) + +/* +struct DownloadStruct { +mut: +	stream  voidptr +	written int +	cb      DownloadFn +} +*/ +fn download_cb(ptr voidptr, size size_t, nmemb size_t, userp voidptr) { +	/* +	mut data := &DownloadStruct(userp) +	written := C.fwrite(ptr, size, nmemb, data.stream) +	data.written += written +	data.cb(data.written) +	//#data->cb(data->written); // TODO +	return written +	*/ +} + +pub fn download_file_with_progress(url string, out string, cb DownloadFn, cb_finished fn ()) { +	/* +	curl := C.curl_easy_init() +	if isnil(curl) { +		return +	} +	cout := out.str +	fp := C.fopen(cout, 'wb') +	C.curl_easy_setopt(curl, CURLOPT_URL, url.str) +	C.curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_cb) +	data := &DownloadStruct { +		stream:fp +		cb: cb +	} +	C.curl_easy_setopt(curl, CURLOPT_WRITEDATA, data) +	mut d := 0.0 +	C.curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &d) +	C.curl_easy_perform(curl) +	C.curl_easy_cleanup(curl) +	C.fclose(fp) +	cb_finished() +	*/ +} + +fn empty() { +} diff --git a/v_windows/v/old/vlib/net/http/download_windows.c.v b/v_windows/v/old/vlib/net/http/download_windows.c.v new file mode 100644 index 0000000..422b6da --- /dev/null +++ b/v_windows/v/old/vlib/net/http/download_windows.c.v @@ -0,0 +1,29 @@ +// 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 + +#flag -l urlmon + +#include <urlmon.h> + +fn download_file_with_progress(url string, out string, cb voidptr, cb_finished voidptr) { +} + +/* +pub fn download_file(url, out string) { +	C.URLDownloadToFile(0, url.to_wide(), out.to_wide(), 0, 0) +	/* +	if (res == S_OK) { +	println('Download Ok') +	# } else if(res == E_OUTOFMEMORY) { +	println('Buffer length invalid, or insufficient memory') +	# } else if(res == INET_E_DOWNLOAD_FAILURE) { +	println('URL is invalid') +	# } else { +	# printf("Download error: %d\n", res); +	# } +	*/ +} +*/ diff --git a/v_windows/v/old/vlib/net/http/header.v b/v_windows/v/old/vlib/net/http/header.v new file mode 100644 index 0000000..e96563e --- /dev/null +++ b/v_windows/v/old/vlib/net/http/header.v @@ -0,0 +1,700 @@ +// 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 = 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] +			} +			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('\r\n') +		} +	} 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('\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') +} diff --git a/v_windows/v/old/vlib/net/http/header_test.v b/v_windows/v/old/vlib/net/http/header_test.v new file mode 100644 index 0000000..3740d8a --- /dev/null +++ b/v_windows/v/old/vlib/net/http/header_test.v @@ -0,0 +1,361 @@ +module http + +fn test_header_new() { +	h := new_header(HeaderConfig{ key: .accept, value: 'nothing' }, +		key: .expires +		value: 'yesterday' +	) +	assert h.contains(.accept) +	assert h.contains(.expires) +	accept := h.get(.accept) or { '' } +	expires := h.get(.expires) or { '' } +	assert accept == 'nothing' +	assert expires == 'yesterday' +} + +fn test_header_invalid_key() { +	mut h := new_header() +	h.add_custom('space is invalid', ':(') or { return } +	panic('should have returned') +} + +fn test_header_adds_multiple() { +	mut h := new_header() +	h.add(.accept, 'one') +	h.add(.accept, 'two') + +	assert h.values(.accept) == ['one', 'two'] +} + +fn test_header_get() ? { +	mut h := new_header(key: .dnt, value: 'one') +	h.add_custom('dnt', 'two') ? +	dnt := h.get_custom('dnt') or { '' } +	exact := h.get_custom('dnt', exact: true) or { '' } +	assert dnt == 'one' +	assert exact == 'two' +} + +fn test_header_set() ? { +	mut h := new_header(HeaderConfig{ key: .dnt, value: 'one' }, +		key: .dnt +		value: 'two' +	) +	assert h.values(.dnt) == ['one', 'two'] +	h.set_custom('DNT', 'three') ? +	assert h.values(.dnt) == ['three'] +} + +fn test_header_delete() { +	mut h := new_header(HeaderConfig{ key: .dnt, value: 'one' }, +		key: .dnt +		value: 'two' +	) +	assert h.values(.dnt) == ['one', 'two'] +	h.delete(.dnt) +	assert h.values(.dnt) == [] +} + +fn test_header_delete_not_existing() { +	mut h := new_header() +	assert h.data.len == 0 +	assert h.keys.len == 0 +	h.delete(.dnt) +	assert h.data.len == 0 +	assert h.keys.len == 0 +} + +fn test_custom_header() ? { +	mut h := new_header() +	h.add_custom('AbC', 'dEf') ? +	h.add_custom('aBc', 'GhI') ? +	assert h.custom_values('AbC', exact: true) == ['dEf'] +	assert h.custom_values('aBc', exact: true) == ['GhI'] +	assert h.custom_values('ABC') == ['dEf', 'GhI'] +	assert h.custom_values('abc') == ['dEf', 'GhI'] +	assert h.keys() == ['AbC', 'aBc'] +	h.delete_custom('AbC') +	h.delete_custom('aBc') + +	h.add_custom('abc', 'def') ? +	assert h.custom_values('abc') == ['def'] +	assert h.custom_values('ABC') == ['def'] +	assert h.keys() == ['abc'] +	h.delete_custom('abc') + +	h.add_custom('accEPT', '*/*') ? +	assert h.custom_values('ACCept') == ['*/*'] +	assert h.values(.accept) == ['*/*'] +	assert h.keys() == ['accEPT'] +} + +fn test_contains_custom() ? { +	mut h := new_header() +	h.add_custom('Hello', 'world') ? +	assert h.contains_custom('hello') +	assert h.contains_custom('HELLO') +	assert h.contains_custom('Hello', exact: true) +	assert h.contains_custom('hello', exact: true) == false +	assert h.contains_custom('HELLO', exact: true) == false +} + +fn test_get_custom() ? { +	mut h := new_header() +	h.add_custom('Hello', 'world') ? +	assert h.get_custom('hello') ? == 'world' +	assert h.get_custom('HELLO') ? == 'world' +	assert h.get_custom('Hello', exact: true) ? == 'world' +	if _ := h.get_custom('hello', exact: true) { +		// should be none +		assert false +	} +	if _ := h.get_custom('HELLO', exact: true) { +		// should be none +		assert false +	} +} + +fn test_starting_with() ? { +	mut h := new_header() +	h.add_custom('Hello-1', 'world') ? +	h.add_custom('Hello-21', 'world') ? +	assert h.starting_with('Hello-') ? == 'Hello-1' +	assert h.starting_with('Hello-2') ? == 'Hello-21' +} + +fn test_custom_values() ? { +	mut h := new_header() +	h.add_custom('Hello', 'world') ? +	assert h.custom_values('hello') == ['world'] +	assert h.custom_values('HELLO') == ['world'] +	assert h.custom_values('Hello', exact: true) == ['world'] +	assert h.custom_values('hello', exact: true) == [] +	assert h.custom_values('HELLO', exact: true) == [] +} + +fn test_coerce() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add(.accept, 'bar') +	assert h.values(.accept) == ['foo', 'bar'] +	assert h.keys().len == 2 + +	h.coerce() +	assert h.values(.accept) == ['foo', 'bar'] +	assert h.keys() == ['accept'] // takes the first occurrence +} + +fn test_coerce_canonicalize() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add(.accept, 'bar') +	assert h.values(.accept) == ['foo', 'bar'] +	assert h.keys().len == 2 + +	h.coerce(canonicalize: true) +	assert h.values(.accept) == ['foo', 'bar'] +	assert h.keys() == ['Accept'] // canonicalize header +} + +fn test_coerce_custom() ? { +	mut h := new_header() +	h.add_custom('Hello', 'foo') ? +	h.add_custom('hello', 'bar') ? +	h.add_custom('HELLO', 'baz') ? +	assert h.custom_values('hello') == ['foo', 'bar', 'baz'] +	assert h.keys().len == 3 + +	h.coerce() +	assert h.custom_values('hello') == ['foo', 'bar', 'baz'] +	assert h.keys() == ['Hello'] // takes the first occurrence +} + +fn test_coerce_canonicalize_custom() ? { +	mut h := new_header() +	h.add_custom('foo-BAR', 'foo') ? +	h.add_custom('FOO-bar', 'bar') ? +	assert h.custom_values('foo-bar') == ['foo', 'bar'] +	assert h.keys().len == 2 + +	h.coerce(canonicalize: true) +	assert h.custom_values('foo-bar') == ['foo', 'bar'] +	assert h.keys() == ['Foo-Bar'] // capitalizes the header +} + +fn test_render_version() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add_custom('Accept', 'bar') ? +	h.add(.accept, 'baz') + +	s1_0 := h.render(version: .v1_0) +	assert s1_0.contains('accept: foo\r\n') +	assert s1_0.contains('Accept: bar,baz\r\n') + +	s1_1 := h.render(version: .v1_1) +	assert s1_1.contains('accept: foo\r\n') +	assert s1_1.contains('Accept: bar,baz\r\n') + +	s2_0 := h.render(version: .v2_0) +	assert s2_0.contains('accept: foo\r\n') +	assert s2_0.contains('accept: bar,baz\r\n') +} + +fn test_render_coerce() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add_custom('Accept', 'bar') ? +	h.add(.accept, 'baz') +	h.add(.host, 'host') + +	s1_0 := h.render(version: .v1_1, coerce: true) +	assert s1_0.contains('accept: foo,bar,baz\r\n') +	assert s1_0.contains('Host: host\r\n') + +	s1_1 := h.render(version: .v1_1, coerce: true) +	assert s1_1.contains('accept: foo,bar,baz\r\n') +	assert s1_1.contains('Host: host\r\n') + +	s2_0 := h.render(version: .v2_0, coerce: true) +	assert s2_0.contains('accept: foo,bar,baz\r\n') +	assert s2_0.contains('host: host\r\n') +} + +fn test_render_canonicalize() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add_custom('Accept', 'bar') ? +	h.add(.accept, 'baz') +	h.add(.host, 'host') + +	s1_0 := h.render(version: .v1_1, canonicalize: true) +	assert s1_0.contains('Accept: foo\r\n') +	assert s1_0.contains('Accept: bar,baz\r\n') +	assert s1_0.contains('Host: host\r\n') + +	s1_1 := h.render(version: .v1_1, canonicalize: true) +	assert s1_1.contains('Accept: foo\r\n') +	assert s1_1.contains('Accept: bar,baz\r\n') +	assert s1_1.contains('Host: host\r\n') + +	s2_0 := h.render(version: .v2_0, canonicalize: true) +	assert s2_0.contains('accept: foo\r\n') +	assert s2_0.contains('accept: bar,baz\r\n') +	assert s2_0.contains('host: host\r\n') +} + +fn test_render_coerce_canonicalize() ? { +	mut h := new_header() +	h.add_custom('accept', 'foo') ? +	h.add_custom('Accept', 'bar') ? +	h.add(.accept, 'baz') +	h.add(.host, 'host') + +	s1_0 := h.render(version: .v1_1, coerce: true, canonicalize: true) +	assert s1_0.contains('Accept: foo,bar,baz\r\n') +	assert s1_0.contains('Host: host\r\n') + +	s1_1 := h.render(version: .v1_1, coerce: true, canonicalize: true) +	assert s1_1.contains('Accept: foo,bar,baz\r\n') +	assert s1_1.contains('Host: host\r\n') + +	s2_0 := h.render(version: .v2_0, coerce: true, canonicalize: true) +	assert s2_0.contains('accept: foo,bar,baz\r\n') +	assert s2_0.contains('host: host\r\n') +} + +fn test_str() ? { +	mut h := new_header() +	h.add(.accept, 'text/html') +	h.add_custom('Accept', 'image/jpeg') ? +	h.add_custom('X-custom', 'Hello') ? + +	// key order is not guaranteed +	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') +} + +fn parse_headers_test(s string, expected map[string]string) ? { +	assert parse_headers(s) ? == new_custom_header_from_map(expected) ? +} + +fn test_parse_headers() ? { +	parse_headers_test('foo: bar', map{ +		'foo': 'bar' +	}) ? +	parse_headers_test('foo: \t  bar', map{ +		'foo': 'bar' +	}) ? +	parse_headers_test('foo: bar\r\n\tbaz', map{ +		'foo': 'bar baz' +	}) ? +	parse_headers_test('foo: bar \r\n\tbaz\r\n   buzz', map{ +		'foo': 'bar baz buzz' +	}) ? +	parse_headers_test('foo: bar\r\nbar:baz', map{ +		'foo': 'bar' +		'bar': 'baz' +	}) ? +	parse_headers_test('foo: bar\r\nbar:baz\r\n', map{ +		'foo': 'bar' +		'bar': 'baz' +	}) ? +	parse_headers_test('foo: bar\r\nbar:baz\r\n\r\n', map{ +		'foo': 'bar' +		'bar': 'baz' +	}) ? +	assert parse_headers('foo: bar\r\nfoo:baz') ?.custom_values('foo') == ['bar', 'baz'] + +	if x := parse_headers(' oops: oh no') { +		return error('should have errored, but got $x') +	} +} diff --git a/v_windows/v/old/vlib/net/http/http.v b/v_windows/v/old/vlib/net/http/http.v new file mode 100644 index 0000000..4f039b9 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/http.v @@ -0,0 +1,175 @@ +// 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.urllib + +const ( +	max_redirects        = 4 +	content_type_default = 'text/plain' +	bufsize              = 1536 +) + +// FetchConfig holds configurations of fetch +pub struct FetchConfig { +pub mut: +	method     Method +	header     Header +	data       string +	params     map[string]string +	cookies    map[string]string +	user_agent string = 'v.http' +	verbose    bool +} + +pub fn new_request(method Method, url_ string, data string) ?Request { +	url := if method == .get { url_ + '?' + data } else { url_ } +	// println('new req() method=$method url="$url" dta="$data"') +	return Request{ +		method: method +		url: url +		data: data +		/* +		headers: { +			'Accept-Encoding': 'compress' +		} +		*/ +	} +} + +// get sends a GET HTTP request to the URL +pub fn get(url string) ?Response { +	return fetch_with_method(.get, url, FetchConfig{}) +} + +// post sends a POST HTTP request to the URL with a string data +pub fn post(url string, data string) ?Response { +	return fetch_with_method(.post, url, +		data: data +		header: new_header(key: .content_type, value: http.content_type_default) +	) +} + +// post_json sends a POST HTTP request to the URL with a JSON data +pub fn post_json(url string, data string) ?Response { +	return fetch_with_method(.post, url, +		data: data +		header: new_header(key: .content_type, value: 'application/json') +	) +} + +// post_form sends a POST HTTP request to the URL with X-WWW-FORM-URLENCODED data +pub fn post_form(url string, data map[string]string) ?Response { +	return fetch_with_method(.post, url, +		header: new_header(key: .content_type, value: 'application/x-www-form-urlencoded') +		data: url_encode_form_data(data) +	) +} + +// put sends a PUT HTTP request to the URL with a string data +pub fn put(url string, data string) ?Response { +	return fetch_with_method(.put, url, +		data: data +		header: new_header(key: .content_type, value: http.content_type_default) +	) +} + +// patch sends a PATCH HTTP request to the URL with a string data +pub fn patch(url string, data string) ?Response { +	return fetch_with_method(.patch, url, +		data: data +		header: new_header(key: .content_type, value: http.content_type_default) +	) +} + +// head sends a HEAD HTTP request to the URL +pub fn head(url string) ?Response { +	return fetch_with_method(.head, url, FetchConfig{}) +} + +// delete sends a DELETE HTTP request to the URL +pub fn delete(url string) ?Response { +	return fetch_with_method(.delete, url, FetchConfig{}) +} + +// fetch sends an HTTP request to the URL with the given method and configurations +pub fn fetch(_url string, config FetchConfig) ?Response { +	if _url == '' { +		return error('http.fetch: empty url') +	} +	url := build_url_from_fetch(_url, config) or { return error('http.fetch: invalid url $_url') } +	data := config.data +	req := Request{ +		method: config.method +		url: url +		data: data +		header: config.header +		cookies: config.cookies +		user_agent: config.user_agent +		user_ptr: 0 +		verbose: config.verbose +	} +	res := req.do() ? +	return res +} + +// get_text sends a GET HTTP request to the URL and returns the text content of the response +pub fn get_text(url string) string { +	resp := fetch(url, method: .get) or { return '' } +	return resp.text +} + +// url_encode_form_data converts mapped data to an URL encoded string +pub fn url_encode_form_data(data map[string]string) string { +	mut pieces := []string{} +	for key_, value_ in data { +		key := urllib.query_escape(key_) +		value := urllib.query_escape(value_) +		pieces << '$key=$value' +	} +	return pieces.join('&') +} + +fn fetch_with_method(method Method, url string, _config FetchConfig) ?Response { +	mut config := _config +	config.method = method +	return fetch(url, config) +} + +fn build_url_from_fetch(_url string, config FetchConfig) ?string { +	mut url := urllib.parse(_url) ? +	if config.params.len == 0 { +		return url.str() +	} +	mut pieces := []string{cap: config.params.len} +	for key, val in config.params { +		pieces << '$key=$val' +	} +	mut query := pieces.join('&') +	if url.raw_query.len > 1 { +		query = url.raw_query + '&' + query +	} +	url.raw_query = query +	return url.str() +} + +// unescape_url is deprecated, use urllib.query_unescape() instead +pub fn unescape_url(s string) string { +	panic('http.unescape_url() was replaced with urllib.query_unescape()') +} + +// escape_url is deprecated, use urllib.query_escape() instead +pub fn escape_url(s string) string { +	panic('http.escape_url() was replaced with urllib.query_escape()') +} + +// unescape is deprecated, use urllib.query_escape() instead +pub fn unescape(s string) string { +	panic('http.unescape() was replaced with http.unescape_url()') +} + +// escape is deprecated, use urllib.query_unescape() instead +pub fn escape(s string) string { +	panic('http.escape() was replaced with http.escape_url()') +} diff --git a/v_windows/v/old/vlib/net/http/http_httpbin_test.v b/v_windows/v/old/vlib/net/http/http_httpbin_test.v new file mode 100644 index 0000000..2023099 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/http_httpbin_test.v @@ -0,0 +1,95 @@ +module http + +// internal tests have access to *everything in the module* +import json + +struct HttpbinResponseBody { +	args    map[string]string +	data    string +	files   map[string]string +	form    map[string]string +	headers map[string]string +	json    map[string]string +	origin  string +	url     string +} + +fn http_fetch_mock(_methods []string, _config FetchConfig) ?[]Response { +	url := 'https://httpbin.org/' +	methods := if _methods.len == 0 { ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] } else { _methods } +	mut config := _config +	mut result := []Response{} +	// Note: httpbin doesn't support head +	for method in methods { +		lmethod := method.to_lower() +		config.method = method_from_str(method) +		res := fetch(url + lmethod, config) ? +		// TODO +		// body := json.decode(HttpbinResponseBody,res.text)? +		result << res +	} +	return result +} + +fn test_http_fetch_bare() { +	$if !network ? { +		return +	} +	responses := http_fetch_mock([], FetchConfig{}) or { panic(err) } +	for response in responses { +		assert response.status() == .ok +	} +} + +fn test_http_fetch_with_data() { +	$if !network ? { +		return +	} +	responses := http_fetch_mock(['POST', 'PUT', 'PATCH', 'DELETE'], +		data: 'hello world' +	) or { panic(err) } +	for response in responses { +		payload := json.decode(HttpbinResponseBody, response.text) or { panic(err) } +		assert payload.data == 'hello world' +	} +} + +fn test_http_fetch_with_params() { +	$if !network ? { +		return +	} +	responses := http_fetch_mock([], +		params: map{ +			'a': 'b' +			'c': 'd' +		} +	) or { panic(err) } +	for response in responses { +		// payload := json.decode(HttpbinResponseBody,response.text) or { +		// panic(err) +		// } +		assert response.status() == .ok +		// TODO +		// assert payload.args['a'] == 'b' +		// assert payload.args['c'] == 'd' +	} +} + +fn test_http_fetch_with_headers() ? { +	$if !network ? { +		return +	} +	mut header := new_header() +	header.add_custom('Test-Header', 'hello world') ? +	responses := http_fetch_mock([], +		header: header +	) or { panic(err) } +	for response in responses { +		// payload := json.decode(HttpbinResponseBody,response.text) or { +		// panic(err) +		// } +		assert response.status() == .ok +		// TODO +		// assert payload.headers['Test-Header'] == 'hello world' +	} +} diff --git a/v_windows/v/old/vlib/net/http/http_test.v b/v_windows/v/old/vlib/net/http/http_test.v new file mode 100644 index 0000000..8b68073 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/http_test.v @@ -0,0 +1,56 @@ +import net.http + +fn test_http_get() { +	$if !network ? { +		return +	} +	assert http.get_text('https://vlang.io/version') == '0.1.5' +	println('http ok') +} + +fn test_http_get_from_vlang_utc_now() { +	$if !network ? { +		return +	} +	urls := ['http://vlang.io/utc_now', 'https://vlang.io/utc_now'] +	for url in urls { +		println('Test getting current time from $url by http.get') +		res := http.get(url) or { panic(err) } +		assert res.status() == .ok +		assert res.text.len > 0 +		assert res.text.int() > 1566403696 +		println('Current time is: $res.text.int()') +	} +} + +fn test_public_servers() { +	$if !network ? { +		return +	} +	urls := [ +		'http://github.com/robots.txt', +		'http://google.com/robots.txt', +		'https://github.com/robots.txt', +		'https://google.com/robots.txt', +		// 'http://yahoo.com/robots.txt', +		// 'https://yahoo.com/robots.txt', +	] +	for url in urls { +		println('Testing http.get on public url: $url ') +		res := http.get(url) or { panic(err) } +		assert res.status() == .ok +		assert res.text.len > 0 +	} +} + +fn test_relative_redirects() { +	$if !network ? { +		return +	} $else { +		return +	} // tempfix periodic: httpbin relative redirects are broken +	res := http.get('https://httpbin.org/relative-redirect/3?abc=xyz') or { panic(err) } +	assert res.status() == .ok +	assert res.text.len > 0 +	assert res.text.contains('"abc": "xyz"') +} diff --git a/v_windows/v/old/vlib/net/http/method.v b/v_windows/v/old/vlib/net/http/method.v new file mode 100644 index 0000000..91c93e1 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/method.v @@ -0,0 +1,48 @@ +// 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 + +// The methods listed here are some of the most used ones, ordered by +// commonality. A comprehensive list is available at: +// https://www.iana.org/assignments/http-methods/http-methods.xhtml +pub enum Method { +	get +	post +	put +	head +	delete +	options +	trace +	connect +	patch +} + +pub fn (m Method) str() string { +	return match m { +		.get { 'GET' } +		.post { 'POST' } +		.put { 'PUT' } +		.head { 'HEAD' } +		.delete { 'DELETE' } +		.options { 'OPTIONS' } +		.trace { 'TRACE' } +		.connect { 'CONNECT' } +		.patch { 'PATCH' } +	} +} + +pub fn method_from_str(m string) Method { +	return match m { +		'GET' { Method.get } +		'POST' { Method.post } +		'PUT' { Method.put } +		'HEAD' { Method.head } +		'DELETE' { Method.delete } +		'OPTIONS' { Method.options } +		'TRACE' { Method.trace } +		'CONNECT' { Method.connect } +		'PATCH' { Method.patch } +		else { Method.get } // should we default to GET? +	} +} diff --git a/v_windows/v/old/vlib/net/http/request.v b/v_windows/v/old/vlib/net/http/request.v new file mode 100644 index 0000000..4664659 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/request.v @@ -0,0 +1,324 @@ +// 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 io +import net +import net.urllib +import strings +import time + +// Request holds information about an HTTP request (either received by +// a server or to be sent by a client) +pub struct Request { +pub mut: +	version    Version = .v1_1 +	method     Method +	header     Header +	cookies    map[string]string +	data       string +	url        string +	user_agent string = 'v.http' +	verbose    bool +	user_ptr   voidptr +	// NOT implemented for ssl connections +	// time = -1 for no timeout +	read_timeout  i64 = 30 * time.second +	write_timeout i64 = 30 * time.second +} + +fn (mut req Request) free() { +	unsafe { req.header.free() } +} + +// add_header adds the key and value of an HTTP request header +// To add a custom header, use add_custom_header +pub fn (mut req Request) add_header(key CommonHeader, val string) { +	req.header.add(key, val) +} + +// add_custom_header adds the key and value of an HTTP request header +// This method may fail if the key contains characters that are not permitted +pub fn (mut req Request) add_custom_header(key string, val string) ? { +	return req.header.add_custom(key, val) +} + +// do will send the HTTP request and returns `http.Response` as soon as the response is recevied +pub fn (req &Request) do() ?Response { +	mut url := urllib.parse(req.url) or { return error('http.Request.do: invalid url $req.url') } +	mut rurl := url +	mut resp := Response{} +	mut no_redirects := 0 +	for { +		if no_redirects == max_redirects { +			return error('http.request.do: maximum number of redirects reached ($max_redirects)') +		} +		qresp := req.method_and_url_to_response(req.method, rurl) ? +		resp = qresp +		if resp.status() !in [.moved_permanently, .found, .see_other, .temporary_redirect, +			.permanent_redirect, +		] { +			break +		} +		// follow any redirects +		mut redirect_url := resp.header.get(.location) or { '' } +		if redirect_url.len > 0 && redirect_url[0] == `/` { +			url.set_path(redirect_url) or { +				return error('http.request.do: invalid path in redirect: "$redirect_url"') +			} +			redirect_url = url.str() +		} +		qrurl := urllib.parse(redirect_url) or { +			return error('http.request.do: invalid URL in redirect "$redirect_url"') +		} +		rurl = qrurl +		no_redirects++ +	} +	return resp +} + +fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) ?Response { +	host_name := url.hostname() +	scheme := url.scheme +	p := url.escaped_path().trim_left('/') +	path := if url.query().len > 0 { '/$p?$url.query().encode()' } else { '/$p' } +	mut nport := url.port().int() +	if nport == 0 { +		if scheme == 'http' { +			nport = 80 +		} +		if scheme == 'https' { +			nport = 443 +		} +	} +	// println('fetch $method, $scheme, $host_name, $nport, $path ') +	if scheme == 'https' { +		// println('ssl_do( $nport, $method, $host_name, $path )') +		res := req.ssl_do(nport, method, host_name, path) ? +		return res +	} else if scheme == 'http' { +		// println('http_do( $nport, $method, $host_name, $path )') +		res := req.http_do('$host_name:$nport', method, path) ? +		return res +	} +	return error('http.request.method_and_url_to_response: unsupported scheme: "$scheme"') +} + +fn (req &Request) build_request_headers(method Method, host_name string, path string) string { +	ua := req.user_agent +	mut uheaders := []string{} +	if !req.header.contains(.host) { +		uheaders << 'Host: $host_name\r\n' +	} +	if !req.header.contains(.user_agent) { +		uheaders << 'User-Agent: $ua\r\n' +	} +	if req.data.len > 0 && !req.header.contains(.content_length) { +		uheaders << 'Content-Length: $req.data.len\r\n' +	} +	for key in req.header.keys() { +		if key == CommonHeader.cookie.str() { +			continue +		} +		val := req.header.custom_values(key).join('; ') +		uheaders << '$key: $val\r\n' +	} +	uheaders << req.build_request_cookies_header() +	version := if req.version == .unknown { Version.v1_1 } else { req.version } +	return '$method $path $version\r\n' + uheaders.join('') + 'Connection: close\r\n\r\n' + req.data +} + +fn (req &Request) build_request_cookies_header() string { +	if req.cookies.keys().len < 1 { +		return '' +	} +	mut cookie := []string{} +	for key, val in req.cookies { +		cookie << '$key=$val' +	} +	cookie << req.header.values(.cookie) +	return 'Cookie: ' + cookie.join('; ') + '\r\n' +} + +fn (req &Request) http_do(host string, method Method, path string) ?Response { +	host_name, _ := net.split_address(host) ? +	s := req.build_request_headers(method, host_name, path) +	mut client := net.dial_tcp(host) ? +	client.set_read_timeout(req.read_timeout) +	client.set_write_timeout(req.write_timeout) +	// TODO this really needs to be exposed somehow +	client.write(s.bytes()) ? +	$if trace_http_request ? { +		eprintln('> $s') +	} +	mut bytes := io.read_all(reader: client) ? +	client.close() ? +	response_text := bytes.bytestr() +	$if trace_http_response ? { +		eprintln('< $response_text') +	} +	return parse_response(response_text) +} + +// referer returns 'Referer' header value of the given request +pub fn (req &Request) referer() string { +	return req.header.get(.referer) or { '' } +} + +// Parse a raw HTTP request into a Request object +pub fn parse_request(mut reader io.BufferedReader) ?Request { +	// request line +	mut line := reader.read_line() ? +	method, target, version := parse_request_line(line) ? + +	// headers +	mut header := new_header() +	line = reader.read_line() ? +	for line != '' { +		key, value := parse_header(line) ? +		header.add_custom(key, value) ? +		line = reader.read_line() ? +	} +	header.coerce(canonicalize: true) + +	// body +	mut body := []byte{} +	if length := header.get(.content_length) { +		n := length.int() +		if n > 0 { +			body = []byte{len: n} +			mut count := 0 +			for count < body.len { +				count += reader.read(mut body[count..]) or { break } +			} +		} +	} + +	return Request{ +		method: method +		url: target.str() +		header: header +		data: body.bytestr() +		version: version +	} +} + +fn parse_request_line(s string) ?(Method, urllib.URL, Version) { +	words := s.split(' ') +	if words.len != 3 { +		return error('malformed request line') +	} +	method := method_from_str(words[0]) +	target := urllib.parse(words[1]) ? +	version := version_from_str(words[2]) +	if version == .unknown { +		return error('unsupported version') +	} + +	return method, target, version +} + +// Parse URL encoded key=value&key=value forms +fn parse_form(body string) map[string]string { +	words := body.split('&') +	mut form := map[string]string{} +	for word in words { +		kv := word.split_nth('=', 2) +		if kv.len != 2 { +			continue +		} +		key := urllib.query_unescape(kv[0]) or { continue } +		val := urllib.query_unescape(kv[1]) or { continue } +		form[key] = val +	} +	return form +	// } +	// todo: parse form-data and application/json +	// ... +} + +struct FileData { +pub: +	filename     string +	content_type string +	data         string +} + +struct UnexpectedExtraAttributeError { +	msg  string +	code int +} + +struct MultiplePathAttributesError { +	msg  string = 'Expected at most one path attribute' +	code int +} + +fn parse_multipart_form(body string, boundary string) (map[string]string, map[string][]FileData) { +	sections := body.split(boundary) +	fields := sections[1..sections.len - 1] +	mut form := map[string]string{} +	mut files := map[string][]FileData{} + +	for field in fields { +		// TODO: do not split into lines; do same parsing for HTTP body +		lines := field.split_into_lines()[1..] +		disposition := parse_disposition(lines[0]) +		// Grab everything between the double quotes +		name := disposition['name'] or { continue } +		// Parse files +		// TODO: filename* +		if 'filename' in disposition { +			filename := disposition['filename'] +			// Parse Content-Type header +			if lines.len == 1 || !lines[1].to_lower().starts_with('content-type:') { +				continue +			} +			mut ct := lines[1].split_nth(':', 2)[1] +			ct = ct.trim_left(' \t') +			data := lines_to_string(field.len, lines, 3, lines.len - 1) +			files[name] << FileData{ +				filename: filename +				content_type: ct +				data: data +			} +			continue +		} +		data := lines_to_string(field.len, lines, 2, lines.len - 1) +		form[name] = data +	} +	return form, files +} + +// Parse the Content-Disposition header of a multipart form +// Returns a map of the key="value" pairs +// Example: parse_disposition('Content-Disposition: form-data; name="a"; filename="b"') == {'name': 'a', 'filename': 'b'} +fn parse_disposition(line string) map[string]string { +	mut data := map[string]string{} +	for word in line.split(';') { +		kv := word.split_nth('=', 2) +		if kv.len != 2 { +			continue +		} +		key, value := kv[0].to_lower().trim_left(' \t'), kv[1] +		if value.starts_with('"') && value.ends_with('"') { +			data[key] = value[1..value.len - 1] +		} else { +			data[key] = value +		} +	} +	return data +} + +[manualfree] +fn lines_to_string(len int, lines []string, start int, end int) string { +	mut sb := strings.new_builder(len) +	for i in start .. end { +		sb.writeln(lines[i]) +	} +	sb.cut_last(1) // last newline +	res := sb.str() +	unsafe { sb.free() } +	return res +} diff --git a/v_windows/v/old/vlib/net/http/request_test.v b/v_windows/v/old/vlib/net/http/request_test.v new file mode 100644 index 0000000..d0baf51 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/request_test.v @@ -0,0 +1,138 @@ +module http + +import io + +struct StringReader { +	text string +mut: +	place int +} + +fn (mut s StringReader) read(mut buf []byte) ?int { +	if s.place >= s.text.len { +		return none +	} +	max_bytes := 100 +	end := if s.place + max_bytes >= s.text.len { s.text.len } else { s.place + max_bytes } +	n := copy(buf, s.text[s.place..end].bytes()) +	s.place += n +	return n +} + +fn reader(s string) &io.BufferedReader { +	return io.new_buffered_reader( +		reader: &StringReader{ +			text: s +		} +	) +} + +fn test_parse_request_not_http() { +	mut reader__ := reader('hello') +	parse_request(mut reader__) or { return } +	panic('should not have parsed') +} + +fn test_parse_request_no_headers() { +	mut reader_ := reader('GET / HTTP/1.1\r\n\r\n') +	req := parse_request(mut reader_) or { panic('did not parse: $err') } +	assert req.method == .get +	assert req.url == '/' +	assert req.version == .v1_1 +} + +fn test_parse_request_two_headers() { +	mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2:  B\r\n\r\n') +	req := parse_request(mut reader_) or { panic('did not parse: $err') } +	assert req.header.custom_values('Test1') == ['a'] +	assert req.header.custom_values('Test2') == ['B'] +} + +fn test_parse_request_two_header_values() { +	mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n') +	req := parse_request(mut reader_) or { panic('did not parse: $err') } +	assert req.header.custom_values('Test1') == ['a; b'] +	assert req.header.custom_values('Test2') == ['c', 'd'] +} + +fn test_parse_request_body() { +	mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: b\r\nContent-Length: 4\r\n\r\nbodyabc') +	req := parse_request(mut reader_) or { panic('did not parse: $err') } +	assert req.data == 'body' +} + +fn test_parse_request_line() { +	method, target, version := parse_request_line('GET /target HTTP/1.1') or { +		panic('did not parse: $err') +	} +	assert method == .get +	assert target.str() == '/target' +	assert version == .v1_1 +} + +fn test_parse_form() { +	assert parse_form('foo=bar&bar=baz') == map{ +		'foo': 'bar' +		'bar': 'baz' +	} +	assert parse_form('foo=bar=&bar=baz') == map{ +		'foo': 'bar=' +		'bar': 'baz' +	} +	assert parse_form('foo=bar%3D&bar=baz') == map{ +		'foo': 'bar=' +		'bar': 'baz' +	} +	assert parse_form('foo=b%26ar&bar=baz') == map{ +		'foo': 'b&ar' +		'bar': 'baz' +	} +	assert parse_form('a=b& c=d') == map{ +		'a':  'b' +		' c': 'd' +	} +	assert parse_form('a=b&c= d ') == map{ +		'a': 'b' +		'c': ' d ' +	} +} + +fn test_parse_multipart_form() { +	boundary := '6844a625b1f0b299' +	names := ['foo', 'fooz'] +	file := 'bar.v' +	ct := 'application/octet-stream' +	contents := ['baz', 'buzz'] +	data := "--------------------------$boundary +Content-Disposition: form-data; name=\"${names[0]}\"; filename=\"$file\" +Content-Type: $ct + +${contents[0]} +--------------------------$boundary +Content-Disposition: form-data; name=\"${names[1]}\" + +${contents[1]} +--------------------------$boundary-- +" +	form, files := parse_multipart_form(data, boundary) +	assert files == map{ +		names[0]: [FileData{ +			filename: file +			content_type: ct +			data: contents[0] +		}] +	} + +	assert form == map{ +		names[1]: contents[1] +	} +} + +fn test_parse_large_body() ? { +	body := 'A'.repeat(101) // greater than max_bytes +	req := 'GET / HTTP/1.1\r\nContent-Length: $body.len\r\n\r\n$body' +	mut reader_ := reader(req) +	result := parse_request(mut reader_) ? +	assert result.data.len == body.len +	assert result.data == body +} diff --git a/v_windows/v/old/vlib/net/http/response.v b/v_windows/v/old/vlib/net/http/response.v new file mode 100644 index 0000000..caa8228 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/response.v @@ -0,0 +1,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') +} diff --git a/v_windows/v/old/vlib/net/http/server.v b/v_windows/v/old/vlib/net/http/server.v new file mode 100644 index 0000000..3804ae3 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/server.v @@ -0,0 +1,80 @@ +// 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 io +import net +import time + +interface Handler { +	handle(Request) Response +} + +pub struct Server { +pub mut: +	port          int           = 8080 +	handler       Handler       = DebugHandler{} +	read_timeout  time.Duration = 30 * time.second +	write_timeout time.Duration = 30 * time.second +} + +pub fn (mut s Server) listen_and_serve() ? { +	if s.handler is DebugHandler { +		eprintln('Server handler not set, using debug handler') +	} +	mut l := net.listen_tcp(.ip6, ':$s.port') ? +	eprintln('Listening on :$s.port') +	for { +		mut conn := l.accept() or { +			eprintln('accept() failed: $err; skipping') +			continue +		} +		conn.set_read_timeout(s.read_timeout) +		conn.set_write_timeout(s.write_timeout) +		// TODO: make concurrent +		s.parse_and_respond(mut conn) +	} +} + +fn (mut s Server) parse_and_respond(mut conn net.TcpConn) { +	defer { +		conn.close() or { eprintln('close() failed: $err') } +	} + +	mut reader := io.new_buffered_reader(reader: conn) +	defer { +		reader.free() +	} +	req := parse_request(mut reader) or { +		$if debug { +			// only show in debug mode to prevent abuse +			eprintln('error parsing request: $err') +		} +		return +	} +	mut resp := s.handler.handle(req) +	if resp.version() == .unknown { +		resp.set_version(req.version) +	} +	conn.write(resp.bytes()) or { eprintln('error sending response: $err') } +} + +// DebugHandler implements the Handler interface by echoing the request +// in the response +struct DebugHandler {} + +fn (d DebugHandler) handle(req Request) Response { +	$if debug { +		eprintln('[$time.now()] $req.method $req.url\n\r$req.header\n\r$req.data - 200 OK') +	} $else { +		eprintln('[$time.now()] $req.method $req.url - 200') +	} +	mut r := Response{ +		text: req.data +		header: req.header +	} +	r.set_status(.ok) +	r.set_version(req.version) +	return r +} diff --git a/v_windows/v/old/vlib/net/http/status.v b/v_windows/v/old/vlib/net/http/status.v new file mode 100644 index 0000000..f4bc9ee --- /dev/null +++ b/v_windows/v/old/vlib/net/http/status.v @@ -0,0 +1,255 @@ +// Copyright (c) 2020 Justin E. Jones. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module http + +// The status codes listed here are based on the comprehensive list, +// available at: +// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +pub enum Status { +	unknown = -1 +	unassigned = 0 +	cont = 100 +	switching_protocols = 101 +	processing = 102 +	checkpoint_draft = 103 +	ok = 200 +	created = 201 +	accepted = 202 +	non_authoritative_information = 203 +	no_content = 204 +	reset_content = 205 +	partial_content = 206 +	multi_status = 207 +	already_reported = 208 +	im_used = 226 +	multiple_choices = 300 +	moved_permanently = 301 +	found = 302 +	see_other = 303 +	not_modified = 304 +	use_proxy = 305 +	switch_proxy = 306 +	temporary_redirect = 307 +	permanent_redirect = 308 +	bad_request = 400 +	unauthorized = 401 +	payment_required = 402 +	forbidden = 403 +	not_found = 404 +	method_not_allowed = 405 +	not_acceptable = 406 +	proxy_authentication_required = 407 +	request_timeout = 408 +	conflict = 409 +	gone = 410 +	length_required = 411 +	precondition_failed = 412 +	request_entity_too_large = 413 +	request_uri_too_long = 414 +	unsupported_media_type = 415 +	requested_range_not_satisfiable = 416 +	expectation_failed = 417 +	im_a_teapot = 418 +	misdirected_request = 421 +	unprocessable_entity = 422 +	locked = 423 +	failed_dependency = 424 +	unordered_collection = 425 +	upgrade_required = 426 +	precondition_required = 428 +	too_many_requests = 429 +	request_header_fields_too_large = 431 +	unavailable_for_legal_reasons = 451 +	client_closed_request = 499 +	internal_server_error = 500 +	not_implemented = 501 +	bad_gateway = 502 +	service_unavailable = 503 +	gateway_timeout = 504 +	http_version_not_supported = 505 +	variant_also_negotiates = 506 +	insufficient_storage = 507 +	loop_detected = 508 +	bandwidth_limit_exceeded = 509 +	not_extended = 510 +	network_authentication_required = 511 +} + +pub fn status_from_int(code int) Status { +	return match code { +		100 { Status.cont } +		101 { Status.switching_protocols } +		102 { Status.processing } +		103 { Status.checkpoint_draft } +		104...199 { Status.unassigned } +		200 { Status.ok } +		201 { Status.created } +		202 { Status.accepted } +		203 { Status.non_authoritative_information } +		204 { Status.no_content } +		205 { Status.reset_content } +		206 { Status.partial_content } +		207 { Status.multi_status } +		208 { Status.already_reported } +		209...225 { Status.unassigned } +		226 { Status.im_used } +		227...299 { Status.unassigned } +		300 { Status.multiple_choices } +		301 { Status.moved_permanently } +		302 { Status.found } +		303 { Status.see_other } +		304 { Status.not_modified } +		305 { Status.use_proxy } +		306 { Status.switch_proxy } +		307 { Status.temporary_redirect } +		308 { Status.permanent_redirect } +		309...399 { Status.unassigned } +		400 { Status.bad_request } +		401 { Status.unauthorized } +		402 { Status.payment_required } +		403 { Status.forbidden } +		404 { Status.not_found } +		405 { Status.method_not_allowed } +		406 { Status.not_acceptable } +		407 { Status.proxy_authentication_required } +		408 { Status.request_timeout } +		409 { Status.conflict } +		410 { Status.gone } +		411 { Status.length_required } +		412 { Status.precondition_failed } +		413 { Status.request_entity_too_large } +		414 { Status.request_uri_too_long } +		415 { Status.unsupported_media_type } +		416 { Status.requested_range_not_satisfiable } +		417 { Status.expectation_failed } +		418 { Status.im_a_teapot } +		419...420 { Status.unassigned } +		421 { Status.misdirected_request } +		422 { Status.unprocessable_entity } +		423 { Status.locked } +		424 { Status.failed_dependency } +		425 { Status.unordered_collection } +		426 { Status.upgrade_required } +		428 { Status.precondition_required } +		429 { Status.too_many_requests } +		431 { Status.request_header_fields_too_large } +		432...450 { Status.unassigned } +		451 { Status.unavailable_for_legal_reasons } +		452...499 { Status.unassigned } +		500 { Status.internal_server_error } +		501 { Status.not_implemented } +		502 { Status.bad_gateway } +		503 { Status.service_unavailable } +		504 { Status.gateway_timeout } +		505 { Status.http_version_not_supported } +		506 { Status.variant_also_negotiates } +		507 { Status.insufficient_storage } +		508 { Status.loop_detected } +		509 { Status.bandwidth_limit_exceeded } +		510 { Status.not_extended } +		511 { Status.network_authentication_required } +		512...599 { Status.unassigned } +		else { Status.unknown } +	} +} + +pub fn (code Status) str() string { +	return match code { +		.cont { 'Continue' } +		.switching_protocols { 'Switching Protocols' } +		.processing { 'Processing' } +		.checkpoint_draft { 'Checkpoint Draft' } +		.ok { 'OK' } +		.created { 'Created' } +		.accepted { 'Accepted' } +		.non_authoritative_information { 'Non Authoritative Information' } +		.no_content { 'No Content' } +		.reset_content { 'Reset Content' } +		.partial_content { 'Partial Content' } +		.multi_status { 'Multi Status' } +		.already_reported { 'Already Reported' } +		.im_used { 'IM Used' } +		.multiple_choices { 'Multiple Choices' } +		.moved_permanently { 'Moved Permanently' } +		.found { 'Found' } +		.see_other { 'See Other' } +		.not_modified { 'Not Modified' } +		.use_proxy { 'Use Proxy' } +		.switch_proxy { 'Switch Proxy' } +		.temporary_redirect { 'Temporary Redirect' } +		.permanent_redirect { 'Permanent Redirect' } +		.bad_request { 'Bad Request' } +		.unauthorized { 'Unauthorized' } +		.payment_required { 'Payment Required' } +		.forbidden { 'Forbidden' } +		.not_found { 'Not Found' } +		.method_not_allowed { 'Method Not Allowed' } +		.not_acceptable { 'Not Acceptable' } +		.proxy_authentication_required { 'Proxy Authentication Required' } +		.request_timeout { 'Request Timeout' } +		.conflict { 'Conflict' } +		.gone { 'Gone' } +		.length_required { 'Length Required' } +		.precondition_failed { 'Precondition Failed' } +		.request_entity_too_large { 'Request Entity Too Large' } +		.request_uri_too_long { 'Request URI Too Long' } +		.unsupported_media_type { 'Unsupported Media Type' } +		.requested_range_not_satisfiable { 'Requested Range Not Satisfiable' } +		.expectation_failed { 'Expectation Failed' } +		.im_a_teapot { 'Im a teapot' } +		.misdirected_request { 'Misdirected Request' } +		.unprocessable_entity { 'Unprocessable Entity' } +		.locked { 'Locked' } +		.failed_dependency { 'Failed Dependency' } +		.unordered_collection { 'Unordered Collection' } +		.upgrade_required { 'Upgrade Required' } +		.precondition_required { 'Precondition Required' } +		.too_many_requests { 'Too Many Requests' } +		.request_header_fields_too_large { 'Request Header Fields Too Large' } +		.unavailable_for_legal_reasons { 'Unavailable For Legal Reasons' } +		.internal_server_error { 'Internal Server Error' } +		.not_implemented { 'Not Implemented' } +		.bad_gateway { 'Bad Gateway' } +		.service_unavailable { 'Service Unavailable' } +		.gateway_timeout { 'Gateway Timeout' } +		.http_version_not_supported { 'HTTP Version Not Supported' } +		.variant_also_negotiates { 'Variant Also Negotiates' } +		.insufficient_storage { 'Insufficient Storage' } +		.loop_detected { 'Loop Detected' } +		.bandwidth_limit_exceeded { 'Bandwidth Limit Exceeded' } +		.not_extended { 'Not Extended' } +		.network_authentication_required { 'Network Authentication Required' } +		.unassigned { 'Unassigned' } +		else { 'Unknown' } +	} +} + +// int converts an assigned and known Status to its integral equivalent. +// if a Status is unknown or unassigned, this method will return zero +pub fn (code Status) int() int { +	if code in [.unknown, .unassigned] { +		return 0 +	} +	return int(code) +} + +// is_valid returns true if the status code is assigned and known +pub fn (code Status) is_valid() bool { +	number := code.int() +	return number >= 100 && number < 600 +} + +// is_error will return true if the status code represents either a client or +// a server error; otherwise will return false +pub fn (code Status) is_error() bool { +	number := code.int() +	return number >= 400 && number < 600 +} + +// is_success will return true if the status code represents either an +// informational, success, or redirection response; otherwise will return false +pub fn (code Status) is_success() bool { +	number := code.int() +	return number >= 100 && number < 400 +} diff --git a/v_windows/v/old/vlib/net/http/status_test.v b/v_windows/v/old/vlib/net/http/status_test.v new file mode 100644 index 0000000..154aec3 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/status_test.v @@ -0,0 +1,49 @@ +module http + +fn test_str() { +	code := Status.bad_gateway +	actual := code.str() +	assert actual == 'Bad Gateway' +} + +fn test_int() { +	code := Status.see_other +	actual := code.int() +	assert actual == 303 +} + +fn test_is_valid() { +	code := Status.gateway_timeout +	actual := code.is_valid() +	assert actual == true +} + +fn test_is_valid_negative() { +	code := Status.unassigned +	actual := code.is_valid() +	assert actual == false +} + +fn test_is_error() { +	code := Status.too_many_requests +	actual := code.is_error() +	assert actual == true +} + +fn test_is_error_negative() { +	code := Status.cont +	actual := code.is_error() +	assert actual == false +} + +fn test_is_success() { +	code := Status.accepted +	actual := code.is_success() +	assert actual == true +} + +fn test_is_success_negative() { +	code := Status.forbidden +	actual := code.is_success() +	assert actual == false +} diff --git a/v_windows/v/old/vlib/net/http/version.v b/v_windows/v/old/vlib/net/http/version.v new file mode 100644 index 0000000..f4388a3 --- /dev/null +++ b/v_windows/v/old/vlib/net/http/version.v @@ -0,0 +1,40 @@ +// 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 + +// The versions listed here are the most common ones. +pub enum Version { +	unknown +	v1_1 +	v2_0 +	v1_0 +} + +pub fn (v Version) str() string { +	return match v { +		.v1_1 { 'HTTP/1.1' } +		.v2_0 { 'HTTP/2.0' } +		.v1_0 { 'HTTP/1.0' } +		.unknown { 'unknown' } +	} +} + +pub fn version_from_str(v string) Version { +	return match v.to_lower() { +		'http/1.1' { Version.v1_1 } +		'http/2.0' { Version.v2_0 } +		'http/1.0' { Version.v1_0 } +		else { Version.unknown } +	} +} + +// protos returns the version major and minor numbers +pub fn (v Version) protos() (int, int) { +	match v { +		.v1_1 { return 1, 1 } +		.v2_0 { return 2, 0 } +		.v1_0 { return 1, 0 } +		.unknown { return 0, 0 } +	} +} | 
