v / vlib / net / http / cookie.v
394 lines · 377 sloc · 9.4 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import time
7import strings
8
9pub struct Cookie {
10pub mut:
11 name string
12 value string
13 path string // optional
14 domain string // optional
15 expires time.Time // optional
16 raw_expires string // for reading cookies only. optional.
17 // max_age=0 means no 'Max-Age' attribute specified.
18 // max_age<0 means delete cookie now, equivalently 'Max-Age: 0'
19 // max_age>0 means Max-Age attribute present and given in seconds
20 max_age int
21 secure bool
22 http_only bool
23 same_site SameSite
24 raw string
25 unparsed []string // Raw text of unparsed attribute-value pairs
26}
27
28// SameSite allows a server to define a cookie attribute making it impossible for
29// the browser to send this cookie along with cross-site requests. The main
30// goal is to mitigate the risk of cross-origin information leakage, and provide
31// some protection against cross-site request forgery attacks.
32//
33// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
34pub enum SameSite {
35 same_site_not_set
36 same_site_default_mode = 1
37 same_site_lax_mode
38 same_site_strict_mode
39 same_site_none_mode
40}
41
42// Parses all "Cookie" values from the header `h` and
43// returns the successfully parsed Cookies.
44//
45// if `filter` isn't empty, only cookies of that name are returned
46pub fn read_cookies(h Header, filter string) []&Cookie {
47 // lines := h['Cookie']
48 lines := h.values(.cookie) // or {
49 if lines.len == 0 {
50 return []
51 }
52 mut cookies := []&Cookie{}
53 for _, line_ in lines {
54 mut line := line_.trim_space()
55 mut part := ''
56 for line.len > 0 {
57 mut semicolon_position :=
58 line.index_any(';') // Store the position of the next semicolon
59 if semicolon_position > 0 { // So, there is a semicolon, let's parse until that position
60 line_parts :=
61 line[..semicolon_position].split(';') // split the line only until that semicolon
62 line = line[(semicolon_position + 1)..] // and then skip everything before the semicolon
63 part = line_parts[0]
64 } else {
65 part = line
66 line = ''
67 }
68 part = part.trim_space()
69 if part.len == 0 {
70 continue
71 }
72 mut name, mut val := part.split_once('=') or { part, '' }
73 if !is_cookie_name_valid(name) {
74 continue
75 }
76 if filter != '' && filter != name {
77 continue
78 }
79 val = parse_cookie_value(val, true) or { continue }
80 cookies << &Cookie{
81 name: name
82 value: val
83 }
84 }
85 }
86 return cookies
87}
88
89// str returns the serialization of the cookie for use in a Cookie header
90// (if only Name and Value are set) or a Set-Cookie response
91// header (if other fields are set).
92//
93// If c.name is invalid, the empty string is returned.
94pub fn (c &Cookie) str() string {
95 if !is_cookie_name_valid(c.name) {
96 return ''
97 }
98 // extra_cookie_length derived from typical length of cookie attributes
99 // see RFC 6265 Sec 4.1.
100 extra_cookie_length := 110
101 mut b := strings.new_builder(c.name.len + c.value.len + c.domain.len + c.path.len +
102 extra_cookie_length)
103 b.write_string(c.name)
104 b.write_string('=')
105 b.write_string(sanitize_cookie_value(c.value))
106 if c.path.len > 0 {
107 b.write_string('; path=')
108 b.write_string(sanitize_cookie_path(c.path))
109 }
110 if c.domain.len > 0 {
111 if valid_cookie_domain(c.domain) {
112 // A `domain` containing illegal characters is not
113 // sanitized but simply dropped which turns the cookie
114 // into a host-only cookie. A leading dot is okay
115 // but won't be sent.
116 mut d := c.domain
117 if d[0] == `.` {
118 d = d.substr(1, d.len)
119 }
120 b.write_string('; domain=')
121 b.write_string(d)
122 } else {
123 // TODO: Log invalid cookie domain warning
124 }
125 }
126 if c.expires.year > 1600 {
127 time_str := c.expires.http_header_string()
128 b.write_string('; expires=')
129 b.write_string(time_str)
130 }
131 // TODO: Fix this. Technically a max age of 0 or less should be 0
132 // We need a way to not have a max age.
133 if c.max_age > 0 {
134 b.write_string('; Max-Age=')
135 b.write_string(c.max_age.str())
136 } else if c.max_age < 0 {
137 b.write_string('; Max-Age=0')
138 }
139 if c.http_only {
140 b.write_string('; HttpOnly')
141 }
142 if c.secure {
143 b.write_string('; Secure')
144 }
145 match c.same_site {
146 .same_site_not_set {}
147 .same_site_default_mode {
148 b.write_string('; SameSite')
149 }
150 .same_site_none_mode {
151 b.write_string('; SameSite=None')
152 }
153 .same_site_lax_mode {
154 b.write_string('; SameSite=Lax')
155 }
156 .same_site_strict_mode {
157 b.write_string('; SameSite=Strict')
158 }
159 }
160
161 return b.str()
162}
163
164fn sanitize(valid fn (u8) bool, v string) string {
165 mut ok := true
166 for i in 0 .. v.len {
167 if valid(v[i]) {
168 continue
169 }
170 // TODO: Warn that we're dropping the invalid byte?
171 ok = false
172 break
173 }
174 if ok {
175 return v.clone()
176 }
177 return v.bytes().filter(valid(it)).bytestr()
178}
179
180fn sanitize_cookie_name(name string) string {
181 return name.replace_each(['\n', '-', '\r', '-'])
182}
183
184// https://tools.ietf.org/html/rfc6265#section-4.1.1
185// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
186// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
187// ; US-ASCII characters excluding CTLs,
188// ; whitespace DQUOTE, comma, semicolon,
189// ; and backslash
190// We loosen this as spaces and commas are common in cookie values
191// but we produce a quoted cookie-value in when value starts or ends
192// with a comma or space.
193pub fn sanitize_cookie_value(v string) string {
194 val := sanitize(valid_cookie_value_byte, v)
195 if v.len == 0 {
196 return v
197 }
198 // Check for the existence of a space, comma or semicolon
199 if val.starts_with(' ') || v.contains(';') || val.ends_with(' ') || val.starts_with(',')
200 || val.ends_with(',') {
201 return '"${v}"'
202 }
203 return v
204}
205
206fn sanitize_cookie_path(v string) string {
207 return sanitize(valid_cookie_path_byte, v)
208}
209
210fn valid_cookie_value_byte(b u8) bool {
211 return 0x20 <= b && b < 0x7f && b != `"` && b != `;` && b != `\\`
212}
213
214fn valid_cookie_path_byte(b u8) bool {
215 return 0x20 <= b && b < 0x7f && b != `!`
216}
217
218fn valid_cookie_domain(v string) bool {
219 if is_cookie_domain_name(v) {
220 return true
221 }
222 // TODO
223 // valid_ip := net.parse_ip(v) or {
224 // false
225 // }
226 // if valid_ip {
227 // return true
228 // }
229 return false
230}
231
232pub fn is_cookie_domain_name(_s string) bool {
233 mut s := _s
234 if s.len == 0 {
235 return false
236 }
237 if s.len > 255 {
238 return false
239 }
240 if s[0] == `.` {
241 s = s.substr(1, s.len)
242 }
243 mut last := `.`
244 mut ok := false
245 mut part_len := 0
246 for i, _ in s {
247 c := s[i]
248 if c.is_letter() {
249 // No '_' allowed here (in contrast to package net).
250 ok = true
251 part_len++
252 } else if `0` <= c && c <= `9` {
253 // fine
254 part_len++
255 } else if c == `-` {
256 // Byte before dash cannot be dot.
257 if last == `.` {
258 return false
259 }
260 part_len++
261 } else if c == `.` {
262 // Byte before dot cannot be dot, dash.
263 if last == `.` || last == `-` {
264 return false
265 }
266 if part_len > 63 || part_len == 0 {
267 return false
268 }
269 part_len = 0
270 } else {
271 return false
272 }
273 last = c
274 }
275 if last == `-` || part_len > 63 {
276 return false
277 }
278 return ok
279}
280
281fn parse_cookie_value(_raw string, allow_double_quote bool) !string {
282 mut raw := _raw
283 // Strip the quotes, if present
284 if allow_double_quote && raw.len > 1 && raw[0] == `"` && raw[raw.len - 1] == `"` {
285 raw = raw.substr(1, raw.len - 1)
286 }
287 for i in 0 .. raw.len {
288 if !valid_cookie_value_byte(raw[i]) {
289 return error('http.cookie: invalid cookie value')
290 }
291 }
292 return raw
293}
294
295fn is_cookie_name_valid(name string) bool {
296 if name == '' {
297 return false
298 }
299 for b in name {
300 if b < 33 || b > 126 {
301 return false
302 }
303 }
304 return true
305}
306
307fn parse_cookie(line string) !Cookie {
308 mut parts := line.trim_space().split(';')
309 if parts.len == 1 && parts[0] == '' {
310 return error('malformed cookie')
311 }
312 parts[0] = parts[0].trim_space()
313 index := parts[0].index('=') or { return error('malformed cookie') }
314 name := parts[0][..index]
315 raw_value := parts[0][index + 1..]
316 if !is_cookie_name_valid(name) {
317 return error('malformed cookie')
318 }
319 value := parse_cookie_value(raw_value, true) or { return error('malformed cookie') }
320 mut c := Cookie{
321 name: name
322 value: value
323 raw: line
324 }
325 for i, _ in parts {
326 parts[i] = parts[i].trim_space()
327 if parts[i].len == 0 {
328 continue
329 }
330 mut attr := parts[i]
331 mut raw_val := ''
332 if ind := parts[i].index('=') {
333 attr = parts[i][..ind]
334 raw_val = parts[i][ind + 1..]
335 }
336 lower_attr := attr.to_lower()
337 val := parse_cookie_value(raw_val, false) or {
338 c.unparsed << parts[i]
339 continue
340 }
341 match lower_attr {
342 'samesite' {
343 lower_val := val.to_lower()
344 match lower_val {
345 'lax' { c.same_site = .same_site_lax_mode }
346 'strict' { c.same_site = .same_site_strict_mode }
347 'none' { c.same_site = .same_site_none_mode }
348 else { c.same_site = .same_site_default_mode }
349 }
350 }
351 'secure' {
352 c.secure = true
353 continue
354 }
355 'httponly' {
356 c.http_only = true
357 continue
358 }
359 'domain' {
360 c.domain = val
361 continue
362 }
363 'max-age' {
364 mut secs := val.int()
365 if secs != 0 && val[0] != `0` {
366 break
367 }
368 if secs <= 0 {
369 secs = -1
370 }
371 c.max_age = secs
372 continue
373 }
374 // TODO: Fix this once time works better
375 // 'expires' {
376 // c.raw_expires = val
377 // mut exptime := time.parse_iso(val)
378 // if exptime.year == 0 {
379 // exptime = time.parse_iso('Mon, 02-Jan-2006 15:04:05 MST')
380 // }
381 // c.expires = exptime
382 // continue
383 // }
384 'path' {
385 c.path = val
386 continue
387 }
388 else {
389 c.unparsed << parts[i]
390 }
391 }
392 }
393 return c
394}
395