github.com/varialus/godfly@v0.0.0-20130904042352-1934f9f095ab/src/pkg/net/http/cookie.go (about)

     1  // Copyright 2009 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"log"
    11  	"net"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  )
    16  
    17  // This implementation is done according to RFC 6265:
    18  //
    19  //    http://tools.ietf.org/html/rfc6265
    20  
    21  // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
    22  // HTTP response or the Cookie header of an HTTP request.
    23  type Cookie struct {
    24  	Name       string
    25  	Value      string
    26  	Path       string
    27  	Domain     string
    28  	Expires    time.Time
    29  	RawExpires string
    30  
    31  	// MaxAge=0 means no 'Max-Age' attribute specified.
    32  	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
    33  	// MaxAge>0 means Max-Age attribute present and given in seconds
    34  	MaxAge   int
    35  	Secure   bool
    36  	HttpOnly bool
    37  	Raw      string
    38  	Unparsed []string // Raw text of unparsed attribute-value pairs
    39  }
    40  
    41  // readSetCookies parses all "Set-Cookie" values from
    42  // the header h and returns the successfully parsed Cookies.
    43  func readSetCookies(h Header) []*Cookie {
    44  	cookies := []*Cookie{}
    45  	for _, line := range h["Set-Cookie"] {
    46  		parts := strings.Split(strings.TrimSpace(line), ";")
    47  		if len(parts) == 1 && parts[0] == "" {
    48  			continue
    49  		}
    50  		parts[0] = strings.TrimSpace(parts[0])
    51  		j := strings.Index(parts[0], "=")
    52  		if j < 0 {
    53  			continue
    54  		}
    55  		name, value := parts[0][:j], parts[0][j+1:]
    56  		if !isCookieNameValid(name) {
    57  			continue
    58  		}
    59  		value, success := parseCookieValue(value)
    60  		if !success {
    61  			continue
    62  		}
    63  		c := &Cookie{
    64  			Name:  name,
    65  			Value: value,
    66  			Raw:   line,
    67  		}
    68  		for i := 1; i < len(parts); i++ {
    69  			parts[i] = strings.TrimSpace(parts[i])
    70  			if len(parts[i]) == 0 {
    71  				continue
    72  			}
    73  
    74  			attr, val := parts[i], ""
    75  			if j := strings.Index(attr, "="); j >= 0 {
    76  				attr, val = attr[:j], attr[j+1:]
    77  			}
    78  			lowerAttr := strings.ToLower(attr)
    79  			parseCookieValueFn := parseCookieValue
    80  			if lowerAttr == "expires" {
    81  				parseCookieValueFn = parseCookieExpiresValue
    82  			}
    83  			val, success = parseCookieValueFn(val)
    84  			if !success {
    85  				c.Unparsed = append(c.Unparsed, parts[i])
    86  				continue
    87  			}
    88  			switch lowerAttr {
    89  			case "secure":
    90  				c.Secure = true
    91  				continue
    92  			case "httponly":
    93  				c.HttpOnly = true
    94  				continue
    95  			case "domain":
    96  				c.Domain = val
    97  				// TODO: Add domain parsing
    98  				continue
    99  			case "max-age":
   100  				secs, err := strconv.Atoi(val)
   101  				if err != nil || secs != 0 && val[0] == '0' {
   102  					break
   103  				}
   104  				if secs <= 0 {
   105  					c.MaxAge = -1
   106  				} else {
   107  					c.MaxAge = secs
   108  				}
   109  				continue
   110  			case "expires":
   111  				c.RawExpires = val
   112  				exptime, err := time.Parse(time.RFC1123, val)
   113  				if err != nil {
   114  					exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
   115  					if err != nil {
   116  						c.Expires = time.Time{}
   117  						break
   118  					}
   119  				}
   120  				c.Expires = exptime.UTC()
   121  				continue
   122  			case "path":
   123  				c.Path = val
   124  				// TODO: Add path parsing
   125  				continue
   126  			}
   127  			c.Unparsed = append(c.Unparsed, parts[i])
   128  		}
   129  		cookies = append(cookies, c)
   130  	}
   131  	return cookies
   132  }
   133  
   134  // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
   135  func SetCookie(w ResponseWriter, cookie *Cookie) {
   136  	w.Header().Add("Set-Cookie", cookie.String())
   137  }
   138  
   139  // String returns the serialization of the cookie for use in a Cookie
   140  // header (if only Name and Value are set) or a Set-Cookie response
   141  // header (if other fields are set).
   142  func (c *Cookie) String() string {
   143  	var b bytes.Buffer
   144  	fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
   145  	if len(c.Path) > 0 {
   146  		fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
   147  	}
   148  	if len(c.Domain) > 0 {
   149  		if validCookieDomain(c.Domain) {
   150  			// A c.Domain containing illegal characters is not
   151  			// sanitized but simply dropped which turns the cookie
   152  			// into a host-only cookie. A leading dot is okay
   153  			// but won't be sent.
   154  			d := c.Domain
   155  			if d[0] == '.' {
   156  				d = d[1:]
   157  			}
   158  			fmt.Fprintf(&b, "; Domain=%s", d)
   159  		} else {
   160  			log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute",
   161  				c.Domain)
   162  		}
   163  	}
   164  	if c.Expires.Unix() > 0 {
   165  		fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
   166  	}
   167  	if c.MaxAge > 0 {
   168  		fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge)
   169  	} else if c.MaxAge < 0 {
   170  		fmt.Fprintf(&b, "; Max-Age=0")
   171  	}
   172  	if c.HttpOnly {
   173  		fmt.Fprintf(&b, "; HttpOnly")
   174  	}
   175  	if c.Secure {
   176  		fmt.Fprintf(&b, "; Secure")
   177  	}
   178  	return b.String()
   179  }
   180  
   181  // readCookies parses all "Cookie" values from the header h and
   182  // returns the successfully parsed Cookies.
   183  //
   184  // if filter isn't empty, only cookies of that name are returned
   185  func readCookies(h Header, filter string) []*Cookie {
   186  	cookies := []*Cookie{}
   187  	lines, ok := h["Cookie"]
   188  	if !ok {
   189  		return cookies
   190  	}
   191  
   192  	for _, line := range lines {
   193  		parts := strings.Split(strings.TrimSpace(line), ";")
   194  		if len(parts) == 1 && parts[0] == "" {
   195  			continue
   196  		}
   197  		// Per-line attributes
   198  		parsedPairs := 0
   199  		for i := 0; i < len(parts); i++ {
   200  			parts[i] = strings.TrimSpace(parts[i])
   201  			if len(parts[i]) == 0 {
   202  				continue
   203  			}
   204  			name, val := parts[i], ""
   205  			if j := strings.Index(name, "="); j >= 0 {
   206  				name, val = name[:j], name[j+1:]
   207  			}
   208  			if !isCookieNameValid(name) {
   209  				continue
   210  			}
   211  			if filter != "" && filter != name {
   212  				continue
   213  			}
   214  			val, success := parseCookieValue(val)
   215  			if !success {
   216  				continue
   217  			}
   218  			cookies = append(cookies, &Cookie{Name: name, Value: val})
   219  			parsedPairs++
   220  		}
   221  	}
   222  	return cookies
   223  }
   224  
   225  // validCookieDomain returns wheter v is a valid cookie domain-value.
   226  func validCookieDomain(v string) bool {
   227  	if isCookieDomainName(v) {
   228  		return true
   229  	}
   230  	if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
   231  		return true
   232  	}
   233  	return false
   234  }
   235  
   236  // isCookieDomainName returns whether s is a valid domain name or a valid
   237  // domain name with a leading dot '.'.  It is almost a direct copy of
   238  // package net's isDomainName.
   239  func isCookieDomainName(s string) bool {
   240  	if len(s) == 0 {
   241  		return false
   242  	}
   243  	if len(s) > 255 {
   244  		return false
   245  	}
   246  
   247  	if s[0] == '.' {
   248  		// A cookie a domain attribute may start with a leading dot.
   249  		s = s[1:]
   250  	}
   251  	last := byte('.')
   252  	ok := false // Ok once we've seen a letter.
   253  	partlen := 0
   254  	for i := 0; i < len(s); i++ {
   255  		c := s[i]
   256  		switch {
   257  		default:
   258  			return false
   259  		case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
   260  			// No '_' allowed here (in contrast to package net).
   261  			ok = true
   262  			partlen++
   263  		case '0' <= c && c <= '9':
   264  			// fine
   265  			partlen++
   266  		case c == '-':
   267  			// Byte before dash cannot be dot.
   268  			if last == '.' {
   269  				return false
   270  			}
   271  			partlen++
   272  		case c == '.':
   273  			// Byte before dot cannot be dot, dash.
   274  			if last == '.' || last == '-' {
   275  				return false
   276  			}
   277  			if partlen > 63 || partlen == 0 {
   278  				return false
   279  			}
   280  			partlen = 0
   281  		}
   282  		last = c
   283  	}
   284  	if last == '-' || partlen > 63 {
   285  		return false
   286  	}
   287  
   288  	return ok
   289  }
   290  
   291  var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
   292  
   293  func sanitizeCookieName(n string) string {
   294  	return cookieNameSanitizer.Replace(n)
   295  }
   296  
   297  // http://tools.ietf.org/html/rfc6265#section-4.1.1
   298  // cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
   299  // cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
   300  //           ; US-ASCII characters excluding CTLs,
   301  //           ; whitespace DQUOTE, comma, semicolon,
   302  //           ; and backslash
   303  func sanitizeCookieValue(v string) string {
   304  	return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
   305  }
   306  
   307  func validCookieValueByte(b byte) bool {
   308  	return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\'
   309  }
   310  
   311  // path-av           = "Path=" path-value
   312  // path-value        = <any CHAR except CTLs or ";">
   313  func sanitizeCookiePath(v string) string {
   314  	return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
   315  }
   316  
   317  func validCookiePathByte(b byte) bool {
   318  	return 0x20 <= b && b < 0x7f && b != ';'
   319  }
   320  
   321  func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
   322  	ok := true
   323  	for i := 0; i < len(v); i++ {
   324  		if valid(v[i]) {
   325  			continue
   326  		}
   327  		log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
   328  		ok = false
   329  		break
   330  	}
   331  	if ok {
   332  		return v
   333  	}
   334  	buf := make([]byte, 0, len(v))
   335  	for i := 0; i < len(v); i++ {
   336  		if b := v[i]; valid(b) {
   337  			buf = append(buf, b)
   338  		}
   339  	}
   340  	return string(buf)
   341  }
   342  
   343  func unquoteCookieValue(v string) string {
   344  	if len(v) > 1 && v[0] == '"' && v[len(v)-1] == '"' {
   345  		return v[1 : len(v)-1]
   346  	}
   347  	return v
   348  }
   349  
   350  func isCookieByte(c byte) bool {
   351  	switch {
   352  	case c == 0x21, 0x23 <= c && c <= 0x2b, 0x2d <= c && c <= 0x3a,
   353  		0x3c <= c && c <= 0x5b, 0x5d <= c && c <= 0x7e:
   354  		return true
   355  	}
   356  	return false
   357  }
   358  
   359  func isCookieExpiresByte(c byte) (ok bool) {
   360  	return isCookieByte(c) || c == ',' || c == ' '
   361  }
   362  
   363  func parseCookieValue(raw string) (string, bool) {
   364  	return parseCookieValueUsing(raw, isCookieByte)
   365  }
   366  
   367  func parseCookieExpiresValue(raw string) (string, bool) {
   368  	return parseCookieValueUsing(raw, isCookieExpiresByte)
   369  }
   370  
   371  func parseCookieValueUsing(raw string, validByte func(byte) bool) (string, bool) {
   372  	raw = unquoteCookieValue(raw)
   373  	for i := 0; i < len(raw); i++ {
   374  		if !validByte(raw[i]) {
   375  			return "", false
   376  		}
   377  	}
   378  	return raw, true
   379  }
   380  
   381  func isCookieNameValid(raw string) bool {
   382  	return strings.IndexFunc(raw, isNotToken) < 0
   383  }