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(`
`, 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) }