package server import ( "fmt" "net/http" "regexp" "strings" "time" ) const authenticationExpirySeconds = 60 * 60 * 24 * 7 // 1 week // authCookieName generates a cookie name based on the host func authCookieName(host string) string { // Replace non-word characters with underscores re := regexp.MustCompile(`\W`) cleanHost := re.ReplaceAllString(host, "_") return fmt.Sprintf("auth_%s", cleanHost) } // utcDateString converts a timestamp to UTC date string func utcDateString(mtime int64) string { return time.Unix(mtime/1000, 0).UTC().Format(time.RFC1123) } // setCookie sets an HTTP cookie func setCookie(w http.ResponseWriter, name, value string, options CookieOptions) { cookie := &http.Cookie{ Name: name, Value: value, Path: options.Path, HttpOnly: options.HttpOnly, Secure: options.Secure, SameSite: parseSameSite(options.SameSite), } if !options.Expires.IsZero() { cookie.Expires = options.Expires } http.SetCookie(w, cookie) } // getCookie gets an HTTP cookie value func getCookie(r *http.Request, name string) string { cookie, err := r.Cookie(name) if err != nil { return "" } return cookie.Value } // deleteCookie deletes an HTTP cookie func deleteCookie(w http.ResponseWriter, name string, options CookieOptions) { cookie := &http.Cookie{ Name: name, Value: "", Path: options.Path, Expires: time.Unix(0, 0), MaxAge: -1, HttpOnly: options.HttpOnly, Secure: options.Secure, SameSite: parseSameSite(options.SameSite), } http.SetCookie(w, cookie) } // parseSameSite maps a CookieOptions.SameSite string to the net/http enum. // Unrecognized or empty values fall back to SameSiteDefaultMode. func parseSameSite(value string) http.SameSite { switch strings.ToLower(value) { case "strict": return http.SameSiteStrictMode case "lax": return http.SameSiteLaxMode case "none": return http.SameSiteNoneMode default: return http.SameSiteDefaultMode } } // isSecureRequest reports whether the request was received over TLS, // accounting for a reverse proxy terminating TLS upstream. func isSecureRequest(r *http.Request) bool { if r.TLS != nil { return true } return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") } // CookieOptions represents cookie configuration options type CookieOptions struct { Path string Expires time.Time HttpOnly bool Secure bool SameSite string } func extractHost(r *http.Request) string { host := r.Host if host == "" { host = r.Header.Get("Host") } return host } // redirectToAuth creates a redirect response to the auth page func redirectToAuth(w http.ResponseWriter, authPath, fromPath, hostURLPrefix string) { var redirectURL string // Try filtering api paths if strings.HasPrefix(fromPath, "/.") || strings.HasSuffix(fromPath, ".md") { w.Header().Set("Location", applyURLPrefix(authPath, hostURLPrefix)) w.WriteHeader(http.StatusUnauthorized) return } else { redirectURL = fmt.Sprintf("%s?from=%s", authPath, fromPath) } w.Header().Set("Location", applyURLPrefix(redirectURL, hostURLPrefix)) w.WriteHeader(http.StatusFound) } // applyURLPrefix applies a URL prefix to a path func applyURLPrefix(path, prefix string) string { if prefix == "" { return path } return prefix + path } // removeURLPrefix removes a URL prefix from a path func removeURLPrefix(path, prefix string) string { if prefix == "" { return path } if strings.HasPrefix(path, prefix) { return path[len(prefix):] } return path } // isExcludedPath checks if a path should be excluded from authentication func isExcludedPath(path string) bool { if path == "/" { return true } // Check if path starts with any item in excludedPaths for _, excludedPath := range excludedPaths { if strings.HasPrefix(path, excludedPath) { return true } } return false } // parseFormValue safely extracts a form value func parseFormValue(r *http.Request, key string) string { return r.FormValue(key) }