package server import ( "net" "net/http" "strings" "time" ) // tokenCookie remembers a caller's token so it does not have to be pasted for // every upload. // // It is HttpOnly, so a script on this origin cannot read it back, which is the // reason to prefer it over localStorage, where any injected script could // exfiltrate the credential. The page never needs to see the value: the server // resolves it and renders who the caller is. const tokenCookie = "uncensored_send_token" // rememberFor is how long a persisted login survives. Tokens are revoked by // deleting them from the token file, so a long window costs nothing. const rememberFor = 365 * 24 * time.Hour // cookieCredential returns the remembered token, if any. func cookieCredential(r *http.Request) string { c, err := r.Cookie(tokenCookie) if err != nil { return "" } return strings.TrimSpace(c.Value) } // credential resolves the caller's token from an explicit header first, then // from the remembered cookie. Upload additionally accepts a form field, which // takes precedence over both. func credential(r *http.Request) string { if t := bearer(r); t != "" { return t } return cookieCredential(r) } // logIn stores the token in a cookie. // // SameSite=Strict is what makes accepting a cookie as a credential safe here: // without it, any site could make the browser post an upload or a deletion with // the cookie attached. Scoping the path to the mount point keeps the credential // out of requests to the rest of the host when running under a subdirectory. // // When persist is false the cookie carries no lifetime and the browser drops it // when it closes, which is the right default on a machine that is not yours. func (s *Server) logIn(w http.ResponseWriter, r *http.Request, token string, persist bool) { c := &http.Cookie{ Name: tokenCookie, Value: token, Path: s.cfg.BasePath, HttpOnly: true, Secure: s.isHTTPS(r), SameSite: http.SameSiteStrictMode, } if persist { c.MaxAge = int(rememberFor.Seconds()) } http.SetCookie(w, c) } // forget clears a remembered token. func (s *Server) forget(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: tokenCookie, Value: "", Path: s.cfg.BasePath, MaxAge: -1, HttpOnly: true, Secure: s.isHTTPS(r), SameSite: http.SameSiteStrictMode, }) } // isHTTPS decides whether the cookie may carry the Secure attribute. Setting it // on a plain-HTTP development server would stop the browser storing the cookie // at all, so it is only set when the connection is genuinely secure. func (s *Server) isHTTPS(r *http.Request) bool { if strings.HasPrefix(s.cfg.PublicURL, "https://") { return true // the operator said so, and they are behind the proxy } if r.TLS != nil { return true } // Believed only from a proxy that is trusted for forwarded headers at all. if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { if ip := net.ParseIP(host); ip != nil && s.cfg.TrustsProxy(ip) { return r.Header.Get("X-Forwarded-Proto") == "https" } } return false }