github.com/GeniusesGroup/libgo@v0.0.0-20220929090155-5ff932cb408e/http/header-set-cookie.go (about)

     1  /* For license and copyright information please see the LEGAL file in the code repository */
     2  
     3  package http
     4  
     5  import (
     6  	"net"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/GeniusesGroup/libgo/convert"
    12  	"github.com/GeniusesGroup/libgo/protocol"
    13  )
    14  
    15  // GetSetCookies parses and returns the Set-Cookie headers.
    16  // By related RFC must exist just one Set-Cookie in each line of header.
    17  // https://tools.ietf.org/html/rfc6265#section-4.1.1
    18  func (h *header) SetCookies() (setCookies []SetCookie) {
    19  	var scs = h.Gets(HeaderKeySetCookie)
    20  	var setCookieCount = len(scs)
    21  	if setCookieCount == 0 {
    22  		return
    23  	}
    24  	setCookies = make([]SetCookie, setCookieCount)
    25  	for i := 0; i < setCookieCount; i++ {
    26  		setCookies[i].Unmarshal(scs[i])
    27  	}
    28  	return
    29  }
    30  
    31  // MarshalSetCookies parses and set given Set-Cookies to the header.
    32  // By related RFC must exist just one Set-Cookie in each line of header.
    33  // https://tools.ietf.org/html/rfc6265#section-4.1.1
    34  func (h *header) MarshalSetCookies(setCookies []SetCookie) {
    35  	var ln = len(setCookies)
    36  	for i := 0; i < ln; i++ {
    37  		h.Add(HeaderKeySetCookie, setCookies[i].Marshal())
    38  	}
    39  }
    40  
    41  /*
    42  SetCookie structure and methods implement by https://tools.ietf.org/html/rfc6265#section-4.1
    43  
    44  MaxAge:
    45  	MaxAge=0 means no 'Max-Age' attribute specified.
    46  	MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
    47  	MaxAge>0 means Max-Age attribute present and given in seconds
    48  */
    49  
    50  // A SetCookie represents an HTTP cookie as sent in the Set-Cookie header of an
    51  // HTTP response or the Cookie header of an HTTP request.
    52  type SetCookie struct {
    53  	Name     string
    54  	Value    string
    55  	Path     string // optional
    56  	Domain   string // optional
    57  	Expires  string // optional
    58  	MaxAge   string // optional
    59  	Secure   bool   // optional
    60  	HTTPOnly bool   // optional
    61  	SameSite string // optional
    62  }
    63  
    64  // CheckAndSanitize use to check if the set-cookie is in standard by related RFCs.
    65  func (sc *SetCookie) CheckAndSanitize() (err protocol.Error) {
    66  	sc.Name, err = sanitizeCookieName(sc.Name)
    67  	if err != nil {
    68  		return
    69  	}
    70  	sc.Value, err = sanitizeCookieValue(sc.Value)
    71  	if err != nil {
    72  		return
    73  	}
    74  	sc.Path, err = sanitizeCookiePath(sc.Path)
    75  	if err != nil {
    76  		return
    77  	}
    78  	sc.Domain, err = sanitizeCookieDomain(sc.Domain)
    79  	return
    80  }
    81  
    82  // GetExpire return the set-cookie expire in time.Time structure.
    83  func (sc *SetCookie) GetExpire() (expTime time.Time) {
    84  	var err error
    85  	expTime, err = time.Parse(time.RFC1123, sc.Expires)
    86  	if err != nil {
    87  		expTime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", sc.Expires)
    88  		if err != nil {
    89  			expTime = time.Time{}
    90  		}
    91  	}
    92  	return
    93  }
    94  
    95  // SetExpire use to set expire time by time.Time instead of raw string!
    96  // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 but don't force or check here!
    97  func (sc *SetCookie) SetExpire(expTime time.Time) {
    98  	sc.Expires = expTime.UTC().Format(TimeFormat)
    99  }
   100  
   101  // GetMaxAge returns Max-Age value in Int instead of raw string!
   102  func (sc *SetCookie) GetMaxAge() (maxAge int) {
   103  	var err error
   104  	maxAge, err = strconv.Atoi(sc.MaxAge)
   105  	if err != nil {
   106  		maxAge = 0
   107  	}
   108  	return
   109  }
   110  
   111  // SetMaxAge use to set Max-Age value by Int instead of raw string!
   112  func (sc *SetCookie) SetMaxAge(maxAge int) {
   113  	if maxAge > 0 {
   114  		sc.MaxAge = strconv.FormatUint(uint64(maxAge), 10)
   115  	} else if maxAge <= 0 {
   116  		sc.MaxAge = "0"
   117  	}
   118  }
   119  
   120  // SetCookieSameSite allows a server define a cookie attribute making it impossible to
   121  // the browser send this cookie along with cross-site requests. The main goal
   122  // is mitigate the risk of cross-origin information leakage, and provides some
   123  // protection against cross-site request forgery attacks.
   124  //
   125  // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
   126  type SetCookieSameSite int
   127  
   128  //
   129  const (
   130  	SetCookieSameSiteDefaultMode SetCookieSameSite = iota + 1
   131  	// Cookies are allowed to be sent with top-level navigations and will be sent along with GET request
   132  	// initiated by third party website. This is the default value in modern browsers.
   133  	SetCookieSameSiteLaxMode
   134  	// Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party websites.
   135  	SetCookieSameSiteStrictMode
   136  	// Cookies will be sent in all contexts, i.e sending cross-origin is allowed.
   137  	SetCookieSameSiteNoneMode
   138  )
   139  
   140  // GetSameSite returns Same-Site value in SetCookieSameSite type instead of raw string!
   141  func (sc *SetCookie) GetSameSite() (sameSite SetCookieSameSite) {
   142  	var lowerVal = strings.ToLower(sc.SameSite)
   143  	switch lowerVal {
   144  	case "lax":
   145  		sameSite = SetCookieSameSiteLaxMode
   146  	case "strict":
   147  		sameSite = SetCookieSameSiteStrictMode
   148  	case "none":
   149  		sameSite = SetCookieSameSiteNoneMode
   150  	default:
   151  		sameSite = SetCookieSameSiteDefaultMode
   152  	}
   153  	return
   154  }
   155  
   156  // SetSameSite use to set Same-Site value by SetCookieSameSite type instead of raw string!
   157  func (sc *SetCookie) SetSameSite(sameSite SetCookieSameSite) {
   158  	switch sameSite {
   159  	case SetCookieSameSiteDefaultMode:
   160  		// TODO::: Why?? Really why standard mix boolean and value attribute!! we use space for boolean!
   161  		sc.SameSite = " "
   162  	case SetCookieSameSiteLaxMode:
   163  		sc.SameSite = "Lax"
   164  	case SetCookieSameSiteStrictMode:
   165  		sc.SameSite = "Strict"
   166  	case SetCookieSameSiteNoneMode:
   167  		sc.SameSite = "none"
   168  	}
   169  }
   170  
   171  // Marshal returns the serialization of the set-cookie.
   172  func (sc *SetCookie) Marshal() string {
   173  	// TODO::: make buffer by needed size.
   174  	var b strings.Builder
   175  
   176  	b.WriteString(sc.Name)
   177  	b.WriteByte('=')
   178  	b.WriteString(sc.Value)
   179  
   180  	if len(sc.Path) > 0 {
   181  		b.WriteString("; Path=")
   182  		b.WriteString(sc.Path)
   183  	}
   184  	if len(sc.Domain) > 0 {
   185  		b.WriteString("; Domain=")
   186  		b.WriteString(sc.Domain)
   187  	}
   188  	if len(sc.Expires) > 0 {
   189  		b.WriteString("; Expires=")
   190  		b.WriteString(sc.Expires)
   191  	}
   192  	if len(sc.MaxAge) > 0 {
   193  		b.WriteString("; Max-Age=")
   194  		b.WriteString(sc.MaxAge)
   195  	}
   196  	if sc.HTTPOnly {
   197  		b.WriteString("; HttpOnly")
   198  	}
   199  	if sc.Secure {
   200  		b.WriteString("; Secure")
   201  	}
   202  	if len(sc.SameSite) > 0 {
   203  		b.WriteString("; SameSite=")
   204  		b.WriteString(sc.SameSite)
   205  	}
   206  	return b.String()
   207  }
   208  
   209  // Unmarshal parse given set-cookie value to sc and return.
   210  // set-cookie value must be in standard or use CheckAndSanitize() if you desire after Unmarshaling!
   211  // In some bad packet may occur panic, handle panic by recover otherwise app will crash and exit!
   212  func (sc *SetCookie) Unmarshal(setCookie string) {
   213  	var index = strings.IndexByte(setCookie, '=')
   214  	// First check no equal(=) sign or empty name or value
   215  	if index < 1 {
   216  		return
   217  	}
   218  	sc.Name = setCookie[:index]
   219  	setCookie = setCookie[index+1:]
   220  
   221  	index = strings.IndexByte(setCookie, ';')
   222  	if index == -1 {
   223  		sc.Value = setCookie
   224  		return
   225  	}
   226  	sc.Value = setCookie[:index]
   227  
   228  	var attr, val string
   229  	var nextSemiColonIndex int = index
   230  	var end bool
   231  	for !end {
   232  		setCookie = setCookie[nextSemiColonIndex+2:] // +2 due to also have a space after semicolon
   233  
   234  		nextSemiColonIndex = strings.IndexByte(setCookie, ';')
   235  		if nextSemiColonIndex == -1 {
   236  			nextSemiColonIndex = len(setCookie)
   237  			end = true
   238  		}
   239  
   240  		index = strings.IndexByte(setCookie, '=')
   241  		if index == -1 {
   242  			// Boolean attribute
   243  			attr = strings.ToLower(setCookie[:nextSemiColonIndex])
   244  		} else {
   245  			// Value attribute
   246  			attr = strings.ToLower(setCookie[:index])
   247  			val = setCookie[index+1 : nextSemiColonIndex]
   248  		}
   249  
   250  		switch attr {
   251  		case "samesite":
   252  			sc.SameSite = val
   253  			continue
   254  		case "secure":
   255  			sc.Secure = true
   256  			continue
   257  		case "httponly":
   258  			sc.HTTPOnly = true
   259  			continue
   260  		case "domain":
   261  			sc.Domain = val
   262  			continue
   263  		case "max-age":
   264  			sc.MaxAge = val
   265  			continue
   266  		case "expires":
   267  			sc.Expires = val
   268  			continue
   269  		case "path":
   270  			sc.Path = val
   271  			continue
   272  		}
   273  	}
   274  }
   275  
   276  // path-av           = "Path=" path-value
   277  // path-value        = <any CHAR except CTLs or ";">
   278  // Don't check for ; due to Unmarshal will panic for bad cookie!!
   279  func sanitizeCookiePath(v string) (path string, err protocol.Error) {
   280  	var ln = len(v)
   281  	var buf = make([]byte, 0, ln)
   282  	var b byte
   283  	for i := 0; i < ln; i++ {
   284  		b = v[i]
   285  		if 0x20 <= b && b < 0x7f {
   286  			buf = append(buf, b)
   287  		} else {
   288  			err = &ErrCookieBadPath
   289  		}
   290  	}
   291  	path = convert.UnsafeByteSliceToString(buf)
   292  	return
   293  }
   294  
   295  // A sc.Domain containing illegal characters is not sanitized but simply dropped which turns the cookie
   296  // into a host-only cookie. A leading dot is okay but won't be sent.
   297  func sanitizeCookieDomain(d string) (domain string, err protocol.Error) {
   298  	var ln = len(d)
   299  	if ln == 0 || ln > 255 {
   300  		return domain, &ErrCookieBadDomain
   301  	}
   302  
   303  	// A cookie a domain attribute may start with a leading dot.
   304  	if d[0] == '.' {
   305  		d = d[1:]
   306  		ln--
   307  	}
   308  
   309  	var last byte = '.'
   310  	var partlen int
   311  	var b byte
   312  	for i := 0; i < ln; i++ {
   313  		b = d[i]
   314  		switch {
   315  		default:
   316  			return domain, &ErrCookieBadDomain
   317  		case 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z':
   318  			// No '_' allowed here (in contrast to package net).
   319  			partlen++
   320  		case '0' <= b && b <= '9':
   321  			// fine
   322  			partlen++
   323  		case b == '-':
   324  			// Byte before dash cannot be dot.
   325  			if last == '.' {
   326  				return domain, &ErrCookieBadDomain
   327  			}
   328  			partlen++
   329  		case b == '.':
   330  			// Byte before dot cannot be dot or dash.
   331  			if last == '.' || last == '-' {
   332  				return domain, &ErrCookieBadDomain
   333  			}
   334  			if partlen > 63 || partlen == 0 {
   335  				return domain, &ErrCookieBadDomain
   336  			}
   337  			partlen = 0
   338  		}
   339  		last = b
   340  	}
   341  	// TODO::: is end . legal??
   342  	if last != '-' && last != '.' && partlen < 64 {
   343  		return d, err
   344  	}
   345  
   346  	if net.ParseIP(d) != nil {
   347  		return d, err
   348  	}
   349  
   350  	return domain, &ErrCookieBadDomain
   351  }