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 }