120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
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.
|
|
//
|
|
// The cookie carries the token sealed, so this is also where an unreadable one
|
|
// - another server's key, or the older plain format - quietly becomes "no
|
|
// session" rather than a credential that cannot be resolved.
|
|
func (s *Server) cookieCredential(r *http.Request) string {
|
|
c, err := r.Cookie(tokenCookie)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return s.sessions.open(strings.TrimSpace(c.Value))
|
|
}
|
|
|
|
// staleSession reports a cookie that is present but will not open: sealed with
|
|
// another server's key, or written in the plain-text format this replaced.
|
|
// There is no session in it, and leaving it in the browser means sending a dead
|
|
// credential on every request for a year, so the page treats it exactly as it
|
|
// treats a revoked token: say so once, and clear it.
|
|
func (s *Server) staleSession(r *http.Request) bool {
|
|
c, err := r.Cookie(tokenCookie)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
value := strings.TrimSpace(c.Value)
|
|
return value != "" && s.sessions.open(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 (s *Server) credential(r *http.Request) string {
|
|
if t := bearer(r); t != "" {
|
|
return t
|
|
}
|
|
return s.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) error {
|
|
sealed, err := s.sessions.seal(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := &http.Cookie{
|
|
Name: tokenCookie,
|
|
Value: sealed,
|
|
Path: s.cfg.BasePath,
|
|
HttpOnly: true,
|
|
Secure: s.isHTTPS(r),
|
|
SameSite: http.SameSiteStrictMode,
|
|
}
|
|
if persist {
|
|
c.MaxAge = int(rememberFor.Seconds())
|
|
}
|
|
http.SetCookie(w, c)
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|