diff options
Diffstat (limited to 'roseh.moe.go')
| -rw-r--r-- | roseh.moe.go | 569 |
1 files changed, 411 insertions, 158 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index 0369740..bd07aed 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -24,15 +24,19 @@ import ( "cmp" "compress/gzip" "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" "crypto/hkdf" "crypto/hmac" "crypto/rand" - "crypto/sha1" + "crypto/rsa" "crypto/sha256" - "crypto/subtle" + "crypto/x509" "embed" "encoding/base64" "encoding/binary" + "encoding/json" "encoding/xml" "errors" "flag" @@ -58,7 +62,6 @@ import ( "github.com/skip2/go-qrcode" "roseh.moe/pkg/oae2" - "roseh.moe/pkg/roseh.moe/internal/pwhash" "roseh.moe/pkg/wordlist" ) @@ -104,7 +107,43 @@ func urlFlag(name, def, doc string) *url.URL { return u } -var notepadPassword, secretKey, authenticatorKey []byte +func sigVerifier(keyBytes []byte) (func(msg, sig []byte) bool, error) { + key, err := x509.ParsePKIXPublicKey(keyBytes) + if err != nil { + return nil, err + } + switch key := key.(type) { + case *ecdsa.PublicKey: + return func(msg, sig []byte) bool { + hash := sha256.Sum256(msg) + return ecdsa.VerifyASN1(key, hash[:], sig) + }, nil + case *rsa.PublicKey: + return func(msg, sig []byte) bool { + hash := sha256.Sum256(msg) + return rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], sig) == nil + }, nil + case ed25519.PublicKey: + return func(msg, sig []byte) bool { + return ed25519.Verify(key, msg, sig) + }, nil + default: + return nil, fmt.Errorf("unsupported key type") + } +} + +func verifyPubKey(pubKey, msg, sig []byte) error { + verify, err := sigVerifier(pubKey) + if err != nil { + return err + } + if !verify(msg, sig) { + return fmt.Errorf("message failed validation") + } + return nil +} + +var secretKey []byte func loadSecrets() error { fileBytes, err := os.ReadFile(*secretsFile) @@ -112,31 +151,17 @@ func loadSecrets() error { return err } var secretsConfig struct { - NotepadPassword string `xml:",attr"` - SecretKey string `xml:",attr"` - AuthKey string `xml:",attr"` + SecretKey string `xml:",attr"` } if err := xml.Unmarshal(fileBytes, &secretsConfig); err != nil { return fmt.Errorf("%s: %s", *secretsFile, err) } - if secretsConfig.NotepadPassword == "" { - return fmt.Errorf("%s: missing NotepadPassword", *secretsFile) - } if secretsConfig.SecretKey == "" { return fmt.Errorf("%s: missing SecretKey", *secretsFile) } - if secretsConfig.AuthKey == "" { - return fmt.Errorf("%s: missing AuthKey", *secretsFile) - } - if notepadPassword, err = base64.StdEncoding.DecodeString(secretsConfig.NotepadPassword); err != nil { - return fmt.Errorf("%s: bad NotepadPassword (try base64)", *secretsFile) - } if secretKey, err = base64.StdEncoding.DecodeString(secretsConfig.SecretKey); err != nil { return fmt.Errorf("%s: bad SecretKey (try base64)", *secretsFile) } - if authenticatorKey, err = base64.StdEncoding.DecodeString(secretsConfig.AuthKey); err != nil { - return fmt.Errorf("%s: bad AuthKey (try base64)", *secretsFile) - } return nil } @@ -155,9 +180,16 @@ type commandConfig struct { Multi bool `xml:",attr"` } +type passkey struct { + ID string `xml:",attr"` + Key string `xml:",attr"` +} + type serviceConfiguration struct { PackageRedirects []packageConfig `xml:"Redirects>Package"` CommandRedirects []commandConfig `xml:"Redirects>Command"` + Passkey []passkey + passkeys map[string][]byte } var ( @@ -188,6 +220,31 @@ func loadConfig() (*serviceConfiguration, error) { if err := xml.Unmarshal(fileBytes, config); err != nil { return nil, err } + names := make(map[string]bool) + config.passkeys = make(map[string][]byte) + namesByID := make(map[string]string) + for _, passkey := range config.Passkey { + if names[passkey.ID] { + return nil, fmt.Errorf("%s: duplicate name %q", *configFile, passkey.ID) + } + names[passkey.ID] = true + keyBytes, err := base64.RawURLEncoding.DecodeString(passkey.Key) + if err != nil { + return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err) + } + if len(keyBytes) < 16 { + return nil, fmt.Errorf("%s: invalid key for passkey %q: bad user ID: too short", *configFile, passkey.ID) + } + id, key := keyBytes[:16], keyBytes[16:] + if _, err := sigVerifier(key); err != nil { + return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err) + } + if _, ok := config.passkeys[string(id)]; ok { + return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[string(id)], passkey.ID, id) + } + config.passkeys[string(id)] = key + namesByID[string(id)] = passkey.ID + } slices.SortFunc(config.PackageRedirects, func(x, y packageConfig) int { return strings.Compare(x.Name, y.Name) }) slices.SortFunc(config.CommandRedirects, func(x, y commandConfig) int { return strings.Compare(x.Name, y.Name) }) serviceConfigMu.Lock() @@ -212,6 +269,11 @@ var ( "host": func() string { return selfURL.Host }, + "xmlEscape": func(s string) string { + buf := new(strings.Builder) + xml.EscapeText(buf, []byte(s)) + return buf.String() + }, }).Parse(outlineString)) ) @@ -347,7 +409,7 @@ func sharedFileHandler(w http.ResponseWriter, r *http.Request) { } func qrHandler(w http.ResponseWriter, r *http.Request) { - text, ok := verify(r.FormValue("t"), "qr") + text, ok := verifyHMAC(r.FormValue("t"), "qr") if !ok { http.Error(w, "Bad signature", http.StatusBadRequest) return @@ -605,7 +667,7 @@ func wormholeUpload(w http.ResponseWriter, r *http.Request) { downloadURL := selfURL.String() + "/wormhole/" + hole if err := wormholeSuccessTemplate.Execute(w, wormholeSuccessTemplateArgs{ URL: downloadURL, - QR: "/qr.png?t=" + url.QueryEscape(downloadURL) + mac(downloadURL, "qr"), + QR: "/qr.png?t=" + url.QueryEscape(signMsg(downloadURL, "qr")), }); err != nil { log.Printf("Warning: wormholeSend: %s", err) } @@ -685,197 +747,346 @@ func cleanHole() error { } } -func mac(msg string, purpose string) string { +func mac(msg []byte, purpose string) []byte { mac := hmac.New(sha256.New, secretKey) io.WriteString(mac, purpose) io.WriteString(mac, ":") - io.WriteString(mac, msg) - return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + mac.Write(msg) + return mac.Sum(nil) +} + +func signMsg(msg, purpose string) string { + return msg + base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose)) } -func verify(signedMsg, purpose string) (string, bool) { +func verifyHMAC(signedMsg, purpose string) (string, bool) { macSize := base64.RawURLEncoding.EncodedLen(sha256.Size) if len(signedMsg) < macSize { return "", false } msg, msgMAC := signedMsg[:len(signedMsg)-macSize], signedMsg[len(signedMsg)-macSize:] - expectedMAC := mac(msg, purpose) + expectedMAC := base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose)) if !hmac.Equal([]byte(msgMAC), []byte(expectedMAC)) { return "", false } return msg, true } -func marshalInt(x uint64) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, x) - for ; len(buf) > 0 && buf[0] == 0; buf = buf[1:] { +const ( + authVersion = "authv3" + authCookieName = "__Secure-a" +) + +func makeHTTPCookie(token string, expiry time.Time) *http.Cookie { + return &http.Cookie{ + Name: authCookieName, + Value: token, + Path: "/", + Domain: selfURL.Hostname(), + Expires: expiry, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Partitioned: true, } - return buf } -func unmarshalInt(b []byte) (uint64, bool) { - if len(b) > 8 { - return 0, false - } - buf := make([]byte, 8) - copy(buf[8-len(b):], b) - return binary.BigEndian.Uint64(buf), true +type authToken struct { + expiry time.Time + userID []byte } -func marshalTime(t time.Time) []byte { - t = t.UTC() - year, month, day := t.Date() - hour, minute, second := t.Clock() - u := uint64(year) - u = u<<4 | uint64(month) - u = u<<5 | uint64(day) - u = u<<5 | uint64(hour) - u = u<<6 | uint64(minute) - u = u<<6 | uint64(second) - return marshalInt(u) +func attachCookie(w http.ResponseWriter, tok *authToken) { + buf := binary.AppendVarint(nil, tok.expiry.Unix()) + buf = append(buf, tok.userID...) + token := signMsg(base64.RawURLEncoding.EncodeToString(buf), authVersion) + http.SetCookie(w, makeHTTPCookie(token, tok.expiry)) } -func unmarshalTime(b []byte) (time.Time, bool) { - u, ok := unmarshalInt(b) +func cookieAuth(r *http.Request) bool { + cookie, err := r.Cookie(authCookieName) + if err != nil { + return false + } + msg, ok := verifyHMAC(cookie.Value, authVersion) if !ok { - return time.Time{}, false + return false } - second, u := int(u&0x3f), u>>6 - minute, u := int(u&0x3f), u>>6 - hour, u := int(u&0x1f), u>>5 - day, u := int(u&0x1f), u>>5 - month, u := time.Month(u&0xf), u>>4 - year := int(u) - return time.Date(year, month, day, hour, minute, second, 0, time.UTC), true + msgBytes, err := base64.RawURLEncoding.DecodeString(msg) + if err != nil { + return false + } + expiryUnix, n := binary.Varint(msgBytes) + if n <= 0 { + return false + } + msgBytes = msgBytes[n:] + expiry := time.Unix(expiryUnix, 0) + if time.Now().After(expiry) { + return false + } + if len(msgBytes) > 0 { + if len(msgBytes) < 16 { + return false + } + userID := msgBytes + config, err := loadConfig() + if err != nil { + log.Printf("Error: failed to load config: %s", err) + return false + } + if _, ok := config.passkeys[string(userID)]; !ok { + return false + } + } + return true } -const authVersion = "authv2" +var ( + challengeMu sync.Mutex + challenge []byte + challengePosed time.Time +) -func makeToken() string { - t := base64.RawURLEncoding.EncodeToString(marshalTime(time.Now())) - return t + mac(t, authVersion) -} +var ( + //go:embed templates/login.html.template + loginTemplateString string + loginTemplate = template.Must(template.New("login").Parse(loginTemplateString)) +) -const authCookieName = "a" +type loginTemplateArgs struct { + Challenge string +} -func attachCookie(w http.ResponseWriter) { - http.SetCookie(w, &http.Cookie{ - Name: authCookieName, - Value: makeToken(), - Path: "/", - Domain: selfURL.Hostname(), - Expires: time.Now().Add(longLoginTimeout), - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Partitioned: true, - }) +func serveLogin(w http.ResponseWriter, r *http.Request) { + if cookieAuth(r) { + redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect") + if !ok { + redirect = "/" + } + http.Redirect(w, r, redirect, http.StatusSeeOther) + return + } + buf := make([]byte, 32) + rand.Read(buf) + challengeMu.Lock() + challenge = buf + challengePosed = time.Now() + challengeMu.Unlock() + if err := loginTemplate.Execute(w, loginTemplateArgs{ + Challenge: base64.StdEncoding.EncodeToString(buf), + }); err != nil { + log.Printf("Warning: login: %s", err) + } } -func cookieAuth(r *http.Request) (time.Duration, bool) { - cookie, err := r.Cookie(authCookieName) +func login(w http.ResponseWriter, r *http.Request) { + challengeMu.Lock() + myChallenge := challenge + challenge = nil + posed := challengePosed + challengeMu.Unlock() + if len(myChallenge) == 0 { + http.Error(w, "Request already consumed", http.StatusConflict) + return + } + if time.Since(posed) > 10*time.Minute { + http.Error(w, "Challenge is too old. Please try again", http.StatusConflict) + return + } + user, err := base64.StdEncoding.DecodeString(r.FormValue("user")) if err != nil { - return 0, false + http.Error(w, "Bad user", http.StatusBadRequest) + return } - msg, ok := verify(cookie.Value, authVersion) - if !ok { - return 0, false + authData, err := base64.StdEncoding.DecodeString(r.FormValue("auth-data")) + if err != nil { + http.Error(w, "Bad auth data", http.StatusBadRequest) + return } - msgBytes, err := base64.RawURLEncoding.DecodeString(msg) + clientData, err := base64.StdEncoding.DecodeString(r.FormValue("client-data")) if err != nil { - return 0, false + http.Error(w, "Bad client data: invalid base64", http.StatusBadRequest) + return + } + var cd struct { + Type string `json:"type"` + Challenge string `json:"challenge"` + Origin string `json:"origin"` + CrossOrigin bool `json:"crossOrigin"` + } + if err := json.Unmarshal(clientData, &cd); err != nil { + http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest) + return } - t, ok := unmarshalTime(msgBytes) + challengeBase64 := base64.RawURLEncoding.EncodeToString(myChallenge) + if cd.Type != "webauthn.get" || cd.Challenge != challengeBase64 || cd.Origin != selfURL.String() || cd.CrossOrigin { + http.Error(w, "Bad client data", http.StatusBadRequest) + return + } + expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname())) + const flagUserPresence, flagUserVerification = 0x01, 0x04 + if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) { + http.Error(w, "Bad authenticator data", http.StatusBadRequest) + return + } + sig, err := base64.StdEncoding.DecodeString(r.FormValue("sig")) + if err != nil { + http.Error(w, "Bad sig", http.StatusBadRequest) + return + } + config, err := loadConfig() + if err != nil { + log.Printf("Error: failed to load config: %s", err) + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + pubKey, ok := config.passkeys[string(user)] if !ok { - return 0, false + http.Error(w, "No user with the given ID", http.StatusBadRequest) + return } - age := time.Since(t) - if age > longLoginTimeout { - return 0, false + clientDataHash := sha256.Sum256(clientData) + if err := verifyPubKey(pubKey, slices.Concat(authData, clientDataHash[:]), sig); err != nil { + http.Error(w, "Challenge failed", http.StatusBadRequest) + return } - return age, true + attachCookie(w, &authToken{ + expiry: time.Now().Add(30 * 24 * time.Hour), + userID: user, + }) + redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect") + if !ok { + redirect = "/" + } + http.Redirect(w, r, redirect, http.StatusSeeOther) } -func totp(t int64) []byte { - ts := make([]byte, 8) - binary.BigEndian.PutUint64(ts, uint64(t)) - hash := hmac.New(sha1.New, authenticatorKey) - hash.Write(ts) - mac := hash.Sum(nil) - offset := mac[len(mac)-1] & 0xf - n := int32(mac[offset])&0x7f<<24 | - int32(mac[offset+1])<<16 | - int32(mac[offset+2])<<8 | - int32(mac[offset+3]) - return strconv.AppendInt(nil, int64(n%1_000_000), 10) -} +var ( + //go:embed templates/register/register.html.template + registerTemplateString string + registerTemplate = template.Must(outlinedTemplate(registerTemplateString)) +) -func checkOTP(auth string) int { - now := time.Now().Unix() / 30 - return subtle.ConstantTimeCompare([]byte(auth), totp(now-1)) | subtle.ConstantTimeCompare([]byte(auth), totp(now)) | subtle.ConstantTimeCompare([]byte(auth), totp(now+1)) +type registerTemplateArgs struct { + ID string + Challenge string } -func checkPassword(password string) int { - hash, err := pwhash.Hash(password, notepadPassword[:pwhash.SaltSize]) - if err != nil { - return 0 +func serveRegister(w http.ResponseWriter, r *http.Request) { + buf := make([]byte, 32+16) + rand.Read(buf) + challenge, userID := buf[:32], buf[32:] + if err := registerTemplate.Execute(w, registerTemplateArgs{ + ID: base64.StdEncoding.EncodeToString(userID), + Challenge: base64.StdEncoding.EncodeToString(challenge), + }); err != nil { + log.Printf("Warning: serveRegister: %s", err) } - return subtle.ConstantTimeCompare(hash, notepadPassword[pwhash.SaltSize:]) } var ( - //go:embed templates/login.html.template - loginTemplateString string - loginTemplate = template.Must(outlinedTemplate(loginTemplateString)) + //go:embed templates/register/register-success.html.template + registerSuccessTemplateString string + registerSuccessTemplate = template.Must(outlinedTemplate(registerSuccessTemplateString)) ) -type loginTemplateArgs struct { - Quick bool - Error bool +type registerSuccessTemplateArgs struct { + ID string + Key string } -func redirectLogin(w http.ResponseWriter, r *http.Request, redirect string) { - redirect += mac(redirect, "redirect") - http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(redirect), http.StatusSeeOther) +func register(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("name") + if username == "" { + http.Error(w, "Passkey name is required", http.StatusBadRequest) + return + } + userID, err := base64.StdEncoding.DecodeString(r.FormValue("id")) + if err != nil { + http.Error(w, "Invalid user ID", http.StatusBadRequest) + return + } + key, err := base64.StdEncoding.DecodeString(r.FormValue("key")) + if err != nil { + http.Error(w, "Invalid key", http.StatusBadRequest) + return + } + if err := registerSuccessTemplate.Execute(w, registerSuccessTemplateArgs{ + ID: r.FormValue("name"), + Key: base64.RawURLEncoding.EncodeToString(slices.Concat(userID, key)), + }); err != nil { + log.Printf("Warning: register: %s", err) + } } -const ( - quickLoginTimeout = 24 * time.Hour - longLoginTimeout = 300 * 24 * time.Hour +var ( + otpMu sync.Mutex + otpLink string + otpGenerated time.Time ) -func serveLogin(w http.ResponseWriter, r *http.Request) { - _, cookieOK := cookieAuth(r) - if err := loginTemplate.Execute(w, loginTemplateArgs{ - Quick: cookieOK, +var ( + //go:embed templates/otp.html.template + otpTemplateString string + otpTemplate = template.Must(outlinedTemplate(otpTemplateString)) +) + +type otpTemplateArgs struct { + URL string + QR string +} + +func serveOTP(w http.ResponseWriter, r *http.Request) { + const nWords = 5 + buf := make([]byte, 2*nWords) + rand.Read(buf) + words := make([]string, nWords) + for i := range len(buf) / 2 { + words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff] + } + l := strings.Join(words, "-") + otpMu.Lock() + otpLink = l + otpGenerated = time.Now() + otpMu.Unlock() + otpURL := selfURL.String() + "/otp/" + l + if err := otpTemplate.Execute(w, otpTemplateArgs{ + URL: otpURL, + QR: "/qr.png?t=" + url.QueryEscape(signMsg(otpURL, "qr")), }); err != nil { - log.Printf("Warning: login: %s", err) + log.Printf("Warning: serveOTP: %s", err) } } -func login(w http.ResponseWriter, r *http.Request) { - otpMatches := checkOTP(r.FormValue("auth")) - passwordMatches := 1 - _, cookieOK := cookieAuth(r) - if !cookieOK { - passwordMatches = checkPassword(r.FormValue("password")) +func otpLogin(w http.ResponseWriter, r *http.Request) { + otpMu.Lock() + myOTP := otpLink + otpLink = "" + generated := otpGenerated + otpMu.Unlock() + reqOTP := r.PathValue("otp") + if reqOTP == "" { + notFound(w, r) + return } - if otpMatches&passwordMatches == 0 { - if err := loginTemplate.Execute(w, loginTemplateArgs{ - Quick: cookieOK, - Error: true, - }); err != nil { - log.Printf("Warning: login: %s", err) - } + myOTPHash := sha256.Sum256([]byte(myOTP)) + reqOTPHash := sha256.Sum256([]byte(reqOTP)) + if !hmac.Equal(myOTPHash[:], reqOTPHash[:]) { + notFound(w, r) return } - attachCookie(w) - redirect, ok := verify(r.FormValue("redirect"), "redirect") - if !ok { - redirect = "/" + if time.Since(generated) > 10*time.Minute { + http.Error(w, "OTP is too old. Please generate a new link", http.StatusConflict) + return } - http.Redirect(w, r, redirect, http.StatusSeeOther) + attachCookie(w, &authToken{expiry: time.Now().Add(24 * time.Hour)}) + fmt.Fprintf(w, "Temporary 24 hour token issued. You are now logged in.\n") +} + +func logout(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, makeHTTPCookie("!", time.Now())) + fmt.Fprintf(w, "You are now logged out.\n") } var ( @@ -1128,18 +1339,26 @@ type authHandler struct { } func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if age, ok := cookieAuth(r); !ok || age > quickLoginTimeout { - log.Printf("Warning: ok age = %v %v", ok, age) + if !cookieAuth(r) { if h.noRedirect { http.Error(w, "not logged in", http.StatusUnauthorized) } else { u := *r.URL - u.Host = r.Host + switch r.Host { + case + "cinema." + selfURL.Host, + "downloads." + selfURL.Host: + + u.Host = r.Host + default: + u.Host = selfURL.Host + } u.Scheme = "http" if *https != "" { u.Scheme = "https" } - redirectLogin(w, r, u.String()) + redirect := signMsg(u.String(), "redirect") + http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(redirect), http.StatusSeeOther) } return } @@ -1179,6 +1398,35 @@ func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.h.ServeHTTP(w, r) } +type removeAuthCookieHandler struct { + h http.Handler +} + +type hstsHandler struct { + h http.Handler +} + +func (h *hstsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if *https != "" { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + } + h.h.ServeHTTP(w, r) +} + +func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + /*var cookies []*http.Cookie + for _, c := range r.Cookies() { + if c.Name != authCookieName { + cookies = append(cookies, c) + } + } + r.Header.Del("Cookie") + for _, c := range cookies { + r.AddCookie(c) + }*/ + h.h.ServeHTTP(w, r) +} + func main() { flag.Parse() @@ -1241,6 +1489,11 @@ func main() { mux.HandleFunc("GET /wormhole/{hole}", wormholeDownload) mux.Handle("GET /login", &timeoutHandler{h: http.HandlerFunc(serveLogin), timeout: 5 * time.Minute}) mux.Handle("POST /login", &timeoutHandler{h: http.HandlerFunc(login), timeout: 5 * time.Minute}) + mux.Handle("GET /logout", &timeoutHandler{h: http.HandlerFunc(logout), timeout: 5 * time.Minute}) + mux.Handle("GET /register", &timeoutHandler{h: http.HandlerFunc(serveRegister), timeout: 5 * time.Minute}) + mux.Handle("POST /register", &timeoutHandler{h: http.HandlerFunc(register), timeout: 5 * time.Minute}) + mux.Handle("GET /otp", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(serveOTP)}, timeout: 5 * time.Minute}) + mux.Handle("GET /otp/{otp}", &timeoutHandler{h: http.HandlerFunc(otpLogin), timeout: 5 * time.Minute}) mux.Handle("GET /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(notepad)}, timeout: 5 * time.Minute}) mux.Handle("POST /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(autosave), noRedirect: true}, timeout: 5 * time.Minute}) mux.Handle("GET /cmd/{$}", &timeoutHandler{h: http.HandlerFunc(cmdIndex), timeout: 5 * time.Minute}) @@ -1255,17 +1508,17 @@ func main() { mux.Handle("/api/", apiMux) mux.Handle("/", &timeoutHandler{h: http.HandlerFunc(sharedFileHandler), timeout: 5 * time.Minute}) serverMux := http.NewServeMux() - serverMux.Handle(selfURL.Hostname()+"/", http.NewCrossOriginProtection().Handler(&gzipHandler{mux})) + serverMux.Handle(selfURL.Hostname()+"/", &gzipHandler{mux}) jellyfinURL, err := url.Parse("http://127.0.0.1:8096") if err != nil { log.Fatal(err) } - serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{ + serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(jellyfinURL) r.SetXForwarded() }, - }}) + }}}) codeMux := http.NewServeMux() cgitStaticFiles := http.FileServer(http.Dir(*cgit)) codeMux.Handle("/cgit.css", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute}) @@ -1281,20 +1534,20 @@ func main() { Env: []string{"CGIT_CONFIG=" + cgitConfigAbsPath}, } cgitCGI.PathLocationHandler = cgitCGI - codeMux.Handle("/", &timeoutHandler{h: cgitCGI, timeout: 5 * time.Minute}) + codeMux.Handle("/", &timeoutHandler{h: &removeAuthCookieHandler{cgitCGI}, timeout: 5 * time.Minute}) serverMux.Handle("code."+selfURL.Hostname()+"/", &gzipHandler{codeMux}) transmissionURL, err := url.Parse("http://127.0.0.1:9091") if err != nil { log.Fatal(err) } - serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{ + serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(transmissionURL) r.SetXForwarded() }, - }}) + }}}) server := &http.Server{ - Handler: &loggingHandler{serverMux}, + Handler: &loggingHandler{http.NewCrossOriginProtection().Handler(&hstsHandler{serverMux})}, ReadHeaderTimeout: 5 * time.Minute, IdleTimeout: 10 * time.Minute, |
