{{.Title}}
{{.Subtitle}}
{{if .Error}}package main import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "html/template" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" ) const portalCookieName = "portal_session" var ( portalUsername = envOr("PORTAL_USERNAME", "admin") portalPassword = envOr("PORTAL_PASSWORD", "changeme") portalCookieDomain = envOr("PORTAL_COOKIE_DOMAIN", ".example.com") portalHomeURL = envOr("PORTAL_HOME_URL", "https://dashy.example.com") portalSessionTTL = 24 * time.Hour portalSecretOnce sync.Once portalSecret []byte ) type portalClaims struct { User string `json:"u"` Expiry int64 `json:"e"` } func sessionSecret() []byte { portalSecretOnce.Do(func() { if raw := strings.TrimSpace(os.Getenv("PORTAL_SESSION_SECRET")); raw != "" { sum := sha256.Sum256([]byte(raw)) portalSecret = sum[:] return } path := filepath.Join(filepath.Dir(confFile), "portal-session.key") if b, err := os.ReadFile(path); err == nil { if decoded, err := hex.DecodeString(strings.TrimSpace(string(b))); err == nil && len(decoded) >= 32 { portalSecret = decoded return } } portalSecret = make([]byte, 32) if _, err := rand.Read(portalSecret); err != nil { sum := sha256.Sum256([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid()))) portalSecret = sum[:] } _ = os.MkdirAll(filepath.Dir(path), 0o755) _ = os.WriteFile(path, []byte(hex.EncodeToString(portalSecret)), 0o600) }) return portalSecret } func signClaims(c portalClaims) (string, error) { payload, err := json.Marshal(c) if err != nil { return "", err } body := base64.RawURLEncoding.EncodeToString(payload) mac := hmac.New(sha256.New, sessionSecret()) _, _ = mac.Write([]byte(body)) sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) return body + "." + sig, nil } func verifyClaims(token string) bool { parts := strings.Split(token, ".") if len(parts) != 2 { return false } mac := hmac.New(sha256.New, sessionSecret()) _, _ = mac.Write([]byte(parts[0])) got, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil || !hmac.Equal(got, mac.Sum(nil)) { return false } payload, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return false } var c portalClaims if json.Unmarshal(payload, &c) != nil { return false } // 用户名可能已被后台改过,按当前配置比较,避免改完账号旧会话仍有效 user, _ := portalAuthConfigured() return c.User == user && c.Expiry >= time.Now().Unix() } func portalAuthenticated(r *http.Request) bool { c, err := r.Cookie(portalCookieName) return err == nil && verifyClaims(c.Value) } func setPortalCookie(w http.ResponseWriter, token string, maxAge int) { http.SetCookie(w, &http.Cookie{ Name: portalCookieName, Value: token, Path: "/", Domain: portalCookieDomain, MaxAge: maxAge, Expires: time.Now().Add(time.Duration(maxAge) * time.Second), HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, }) } func secureEqual(a, b string) bool { if len(a) != len(b) { return false } return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } func safePortalRedirect(raw string) string { if strings.TrimSpace(raw) == "" { return portalHomeURL } u, err := url.Parse(raw) if err != nil || u.Scheme != "https" || u.Hostname() == "" { return portalHomeURL } cfg, err := loadGateway() if err != nil { return portalHomeURL } for _, r := range cfg.Routes { if strings.EqualFold(strings.TrimSpace(r.Host), u.Hostname()) { return u.String() } } return portalHomeURL } var portalLoginTemplate = template.Must(template.New("login").Parse(`
{{.Subtitle}}
{{if .Error}}