Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole/wormhole/internal/cache"
|
||||
"github.com/wormhole/wormhole/internal/config"
|
||||
"github.com/wormhole/wormhole/internal/store"
|
||||
)
|
||||
|
||||
const accessCookieName = "wh_access"
|
||||
|
||||
type ResourceAuth struct {
|
||||
cache *cache.Manager
|
||||
store *store.Store
|
||||
jwtSecret string
|
||||
limiter *LoginLimiter
|
||||
mu sync.RWMutex
|
||||
grants map[string]time.Time
|
||||
}
|
||||
|
||||
func NewResourceAuth(c *cache.Manager, st *store.Store, authCfg *config.AuthConfig) *ResourceAuth {
|
||||
ra := &ResourceAuth{
|
||||
cache: c,
|
||||
store: st,
|
||||
jwtSecret: authCfg.JWTSecret,
|
||||
limiter: NewLoginLimiter(authCfg),
|
||||
grants: make(map[string]time.Time),
|
||||
}
|
||||
go ra.cleanupLoop()
|
||||
return ra
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) ReloadAuth(cfg *config.AuthConfig) {
|
||||
ra.mu.Lock()
|
||||
defer ra.mu.Unlock()
|
||||
ra.jwtSecret = cfg.JWTSecret
|
||||
ra.limiter = NewLoginLimiter(cfg)
|
||||
}
|
||||
|
||||
func grantKey(resourceType string, resourceID uint, ip string) string {
|
||||
return fmt.Sprintf("%s:%d:%s", resourceType, resourceID, ip)
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) cleanupLoop() {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
for range ticker.C {
|
||||
now := time.Now()
|
||||
ra.mu.Lock()
|
||||
for k, exp := range ra.grants {
|
||||
if now.After(exp) {
|
||||
delete(ra.grants, k)
|
||||
}
|
||||
}
|
||||
ra.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) GrantAccess(resourceType string, resourceID uint, ip string, ttl time.Duration) {
|
||||
ra.mu.Lock()
|
||||
defer ra.mu.Unlock()
|
||||
ra.grants[grantKey(resourceType, resourceID, ip)] = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) HasAccess(resourceType string, resourceID uint, ip string) bool {
|
||||
ra.mu.RLock()
|
||||
defer ra.mu.RUnlock()
|
||||
exp, ok := ra.grants[grantKey(resourceType, resourceID, ip)]
|
||||
return ok && time.Now().Before(exp)
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) CheckHTTP(w http.ResponseWriter, r *http.Request, resourceType string, resourceID uint) bool {
|
||||
cfg, userIDs, ok := GetVisitorConfig(ra.cache, resourceType, resourceID)
|
||||
if ok && cfg.Enabled {
|
||||
return ra.CheckVisitorHTTP(w, r, resourceType, resourceID, cfg, userIDs)
|
||||
}
|
||||
|
||||
ip := DirectRemoteIP(r)
|
||||
if ra.HasAccess(resourceType, resourceID, ip) {
|
||||
return true
|
||||
}
|
||||
policy, ok := ra.cache.GetAuthPolicy(resourceType, resourceID)
|
||||
if !ok || !policy.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
switch policy.Type {
|
||||
case "basic":
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if ok && subtle.ConstantTimeCompare([]byte(pass), []byte(policy.Password)) == 1 {
|
||||
ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Wormhole"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return false
|
||||
case "form":
|
||||
if r.URL.Path == "/_wormhole/auth" && r.Method == http.MethodPost {
|
||||
_ = r.ParseForm()
|
||||
pass := r.FormValue("password")
|
||||
if subtle.ConstantTimeCompare([]byte(pass), []byte(policy.Password)) == 1 {
|
||||
ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
|
||||
http.SetCookie(w, &http.Cookie{Name: accessCookieName, Value: "1", Path: "/", MaxAge: policy.TTLSeconds})
|
||||
redirect := SafeRedirectPath(r.FormValue("redirect"))
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if cookie, err := r.Cookie(accessCookieName); err == nil && cookie.Value == "1" {
|
||||
ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
|
||||
return true
|
||||
}
|
||||
redirect := r.URL.RequestURI()
|
||||
html := fmt.Sprintf(`<!DOCTYPE html><html><body>
|
||||
<form method="POST" action="/_wormhole/auth">
|
||||
<input type="hidden" name="redirect" value="%s"/>
|
||||
<input type="password" name="password" placeholder="Password"/>
|
||||
<button type="submit">Login</button>
|
||||
</form></body></html>`, redirect)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(html))
|
||||
return false
|
||||
case "callback":
|
||||
if policy.CallbackURL != "" {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token != "" && token == policy.Password {
|
||||
ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
|
||||
return true
|
||||
}
|
||||
cb := policy.CallbackURL
|
||||
if stringsContains(cb, "?") {
|
||||
cb += "&"
|
||||
} else {
|
||||
cb += "?"
|
||||
}
|
||||
cb += "redirect=" + r.URL.RequestURI()
|
||||
http.Redirect(w, r, cb, http.StatusFound)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) CheckTCP(remoteAddr net.Addr, resourceType string, resourceID uint) bool {
|
||||
ip := remoteAddr.String()
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
if ra.HasAccess(resourceType, resourceID, ip) {
|
||||
return true
|
||||
}
|
||||
policy, ok := ra.cache.GetAuthPolicy(resourceType, resourceID)
|
||||
if !ok || !policy.Enabled {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stringsContains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (ra *ResourceAuth) CreatePolicy(st *store.Store, p *store.AuthPolicy) error {
|
||||
return st.CreateAuthPolicy(p)
|
||||
}
|
||||
Reference in New Issue
Block a user