github.com/ader1990/go@v0.0.0-20140630135419-8c24447fa791/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  			val, success = parseCookieValue(val)
    80  			if !success {
    81  				c.Unparsed = append(c.Unparsed, parts[i])
    82  				continue
    83  			}
    84  			switch lowerAttr {
    85  			case "secure":
    86  				c.Secure = true
    87  				continue
    88  			case "httponly":
    89  				c.HttpOnly = true
    90  				continue
    91  			case "domain":
    92  				c.Domain = val
    93  				continue
    94  			case "max-age":
    95  				secs, err := strconv.Atoi(val)
    96  				if err != nil || secs != 0 && val[0] == '0' {
    97  					break
    98  				}
    99  				if secs <= 0 {
   100  					c.MaxAge = -1
   101  				} else {
   102  					c.MaxAge = secs
   103  				}
   104  				continue
   105  			case "expires":
   106  				c.RawExpires = val
   107  				exptime, err := time.Parse(time.RFC1123, val)
   108  				if err != nil {
   109  					exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
   110  					if err != nil {
   111  						c.Expires = time.Time{}
   112  						break
   113  					}
   114  				}
   115  				c.Expires = exptime.UTC()
   116  				continue
   117  			case "path":
   118  				c.Path = val
   119  				continue
   120  			}
   121  			c.Unparsed = append(c.Unparsed, parts[i])
   122  		}
   123  		cookies = append(cookies, c)
   124  	}
   125  	return cookies
   126  }
   127  
   128  // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
   129  func SetCookie(w ResponseWriter, cookie *Cookie) {
   130  	w.Header().Add("Set-Cookie", cookie.String())
   131  }
   132  
   133  // String returns the serialization of the cookie for use in a Cookie
   134  // header (if only Name and Value are set) or a Set-Cookie response
   135  // header (if other fields are set).
   136  func (c *Cookie) String() string {
   137  	var b bytes.Buffer
   138  	fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
   139  	if len(c.Path) > 0 {
   140  		fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
   141  	}
   142  	if len(c.Domain) > 0 {
   143  		if validCookieDomain(c.Domain) {
   144  			// A c.Domain containing illegal characters is not
   145  			// sanitized but simply dropped which turns the cookie
   146  			// into a host-only cookie. A leading dot is okay
   147  			// but won't be sent.
   148  			d := c.Domain
   149  			if d[0] == '.' {
   150  				d = d[1:]
   151  			}
   152  			fmt.Fprintf(&b, "; Domain=%s", d)
   153  		} else {
   154  			log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute",
   155  				c.Domain)
   156  		}
   157  	}
   158  	if c.Expires.Unix() > 0 {
   159  		fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
   160  	}
   161  	if c.MaxAge > 0 {
   162  		fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge)
   163  	} else if c.MaxAge < 0 {
   164  		fmt.Fprintf(&b, "; Max-Age=0")
   165  	}
   166  	if c.HttpOnly {
   167  		fmt.Fprintf(&b, "; HttpOnly")
   168  	}
   169  	if c.Secure {
   170  		fmt.Fprintf(&b, "; Secure")
   171  	}
   172  	return b.String()
   173  }
   174  
   175  // readCookies parses all "Cookie" values from the header h and
   176  // returns the successfully parsed Cookies.
   177  //
   178  // if filter isn't empty, only cookies of that name are returned
   179  func readCookies(h Header, filter string) []*Cookie {
   180  	cookies := []*Cookie{}
   181  	lines, ok := h["Cookie"]
   182  	if !ok {
   183  		return cookies
   184  	}
   185  
   186  	for _, line := range lines {
   187  		parts := strings.Split(strings.TrimSpace(line), ";")
   188  		if len(parts) == 1 && parts[0] == "" {
   189  			continue
   190  		}
   191  		// Per-line attributes
   192  		parsedPairs := 0
   193  		for i := 0; i < len(parts); i++ {
   194  			parts[i] = strings.TrimSpace(parts[i])
   195  			if len(parts[i]) == 0 {
   196  				continue
   197  			}
   198  			name, val := parts[i], ""
   199  			if j := strings.Index(name, "="); j >= 0 {
   200  				name, val = name[:j], name[j+1:]
   201  			}
   202  			if !isCookieNameValid(name) {
   203  				continue
   204  			}
   205  			if filter != "" && filter != name {
   206  				continue
   207  			}
   208  			val, success := parseCookieValue(val)
   209  			if !success {
   210  				continue
   211  			}
   212  			cookies = append(cookies, &Cookie{Name: name, Value: val})
   213  			parsedPairs++
   214  		}
   215  	}
   216  	return cookies
   217  }
   218  
   219  // validCookieDomain returns wheter v is a valid cookie domain-value.
   220  func validCookieDomain(v string) bool {
   221  	if isCookieDomainName(v) {
   222  		return true
   223  	}
   224  	if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
   225  		return true
   226  	}
   227  	return false
   228  }
   229  
   230  // isCookieDomainName returns whether s is a valid domain name or a valid
   231  // domain name with a leading dot '.'.  It is almost a direct copy of
   232  // package net's isDomainName.
   233  func isCookieDomainName(s string) bool {
   234  	if len(s) == 0 {
   235  		return false
   236  	}
   237  	if len(s) > 255 {
   238  		return false
   239  	}
   240  
   241  	if s[0] == '.' {
   242  		// A cookie a domain attribute may start with a leading dot.
   243  		s = s[1:]
   244  	}
   245  	last := byte('.')
   246  	ok := false // Ok once we've seen a letter.
   247  	partlen := 0
   248  	for i := 0; i < len(s); i++ {
   249  		c := s[i]
   250  		switch {
   251  		default:
   252  			return false
   253  		case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
   254  			// No '_' allowed here (in contrast to package net).
   255  			ok = true
   256  			partlen++
   257  		case '0' <= c && c <= '9':
   258  			// fine
   259  			partlen++
   260  		case c == '-':
   261  			// Byte before dash cannot be dot.
   262  			if last == '.' {
   263  				return false
   264  			}
   265  			partlen++
   266  		case c == '.':
   267  			// Byte before dot cannot be dot, dash.
   268  			if last == '.' || last == '-' {
   269  				return false
   270  			}
   271  			if partlen > 63 || partlen == 0 {
   272  				return false
   273  			}
   274  			partlen = 0
   275  		}
   276  		last = c
   277  	}
   278  	if last == '-' || partlen > 63 {
   279  		return false
   280  	}
   281  
   282  	return ok
   283  }
   284  
   285  var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
   286  
   287  func sanitizeCookieName(n string) string {
   288  	return cookieNameSanitizer.Replace(n)
   289  }
   290  
   291  // http://tools.ietf.org/html/rfc6265#section-4.1.1
   292  // cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
   293  // cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
   294  //           ; US-ASCII characters excluding CTLs,
   295  //           ; whitespace DQUOTE, comma, semicolon,
   296  //           ; and backslash
   297  // We loosen this as spaces and commas are common in cookie values
   298  // but we produce a quoted cookie-value in when value starts or ends
   299  // with a comma or space.
   300  // See http://golang.org/issue/7243 for the discussion.
   301  func sanitizeCookieValue(v string) string {
   302  	v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
   303  	if len(v) == 0 {
   304  		return v
   305  	}
   306  	if v[0] == ' ' || v[0] == ',' || v[len(v)-1] == ' ' || v[len(v)-1] == ',' {
   307  		return `"` + v + `"`
   308  	}
   309  	return v
   310  }
   311  
   312  func validCookieValueByte(b byte) bool {
   313  	return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
   314  }
   315  
   316  // path-av           = "Path=" path-value
   317  // path-value        = <any CHAR except CTLs or ";">
   318  func sanitizeCookiePath(v string) string {
   319  	return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
   320  }
   321  
   322  func validCookiePathByte(b byte) bool {
   323  	return 0x20 <= b && b < 0x7f && b != ';'
   324  }
   325  
   326  func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
   327  	ok := true
   328  	for i := 0; i < len(v); i++ {
   329  		if valid(v[i]) {
   330  			continue
   331  		}
   332  		log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
   333  		ok = false
   334  		break
   335  	}
   336  	if ok {
   337  		return v
   338  	}
   339  	buf := make([]byte, 0, len(v))
   340  	for i := 0; i < len(v); i++ {
   341  		if b := v[i]; valid(b) {
   342  			buf = append(buf, b)
   343  		}
   344  	}
   345  	return string(buf)
   346  }
   347  
   348  func parseCookieValue(raw string) (string, bool) {
   349  	// Strip the quotes, if present.
   350  	if len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
   351  		raw = raw[1 : len(raw)-1]
   352  	}
   353  	for i := 0; i < len(raw); i++ {
   354  		if !validCookieValueByte(raw[i]) {
   355  			return "", false
   356  		}
   357  	}
   358  	return raw, true
   359  }
   360  
   361  func isCookieNameValid(raw string) bool {
   362  	return strings.IndexFunc(raw, isNotToken) < 0
   363  }