aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/net/http/cookie.v
diff options
context:
space:
mode:
Diffstat (limited to 'v_windows/v/vlib/net/http/cookie.v')
-rw-r--r--v_windows/v/vlib/net/http/cookie.v413
1 files changed, 413 insertions, 0 deletions
diff --git a/v_windows/v/vlib/net/http/cookie.v b/v_windows/v/vlib/net/http/cookie.v
new file mode 100644
index 0000000..d647b3d
--- /dev/null
+++ b/v_windows/v/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
+}