package api import ( "linkport/internal/model" "linkport/internal/security" "net/http" "time" ) func (s *Server) logout(w http.ResponseWriter, sessionID string) { s.mu.Lock() delete(s.sessions, sessionID) s.mu.Unlock() http.SetCookie(w, &http.Cookie{Name: "linkport_session", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode}) respond(w, 200, map[string]bool{"ok": true}) } func (s *Server) limited(key string, max int) bool { s.mu.Lock() defer s.mu.Unlock() now := time.Now() for k, a := range s.attempts { if now.After(a.Until) { delete(s.attempts, k) } } a := s.attempts[key] if a.Until.IsZero() { a.Until = now.Add(5 * time.Minute) } a.Count++ s.attempts[key] = a return a.Count > max } func (s *Server) login(w http.ResponseWriter, r *http.Request) { if s.limited("login:"+source(r), 10) { fail(w, 429, "too many login attempts; retry in 5 minutes") return } var b struct { Username string `json:"username"` Password string `json:"password"` } if !decode(w, r, &b) { return } rev, ok := s.store.Verify(b.Username, b.Password) if !ok { fail(w, 401, "invalid credentials") return } token := security.Token() sess := session{User: b.Username, CSRF: security.Token(), Revision: rev, Expires: time.Now().Add(12 * time.Hour)} s.mu.Lock() for k, v := range s.sessions { if time.Now().After(v.Expires) { delete(s.sessions, k) } } s.sessions[token] = sess s.mu.Unlock() http.SetCookie(w, &http.Cookie{Name: "linkport_session", Value: token, Path: "/", HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteStrictMode, MaxAge: 43200}) respond(w, 200, map[string]string{"username": sess.User, "csrfToken": sess.CSRF}) } func (s *Server) reveal(w http.ResponseWriter, r *http.Request, p model.Proxy, sess session, token string, allowedFields ...string) { a := s.limited("reveal:ip:"+source(r), 5) b := s.limited("reveal:session:"+token, 5) if a || b { s.store.Event(p.ID, source(r), "secret_view_denied", "rate_limit") fail(w, 429, "too many attempts; retry in 5 minutes") return } var body struct { Field string `json:"field"` Password string `json:"password"` } if !decode(w, r, &body) { return } if len(allowedFields) > 0 { allowed := false for _, field := range allowedFields { if field == body.Field { allowed = true } } if !allowed { fail(w, 400, "unsupported secret field") return } } secrets := map[string]string{"targetPassword": p.TargetPassword, "gatewayPassword": p.GatewayPassword, "socksPassword": p.Outbound.Password, "sshPassphrase": p.SSH.Passphrase} for _, account := range p.GatewayAccounts { secrets["gatewayAccount:"+account.ID+":password"] = account.Password } value, exists := secrets[body.Field] if !exists { fail(w, 400, "unsupported secret field") return } if _, ok := s.store.Verify(sess.User, body.Password); s.authEnabled && !ok { s.store.Event(p.ID, source(r), "secret_view_denied", body.Field) fail(w, 401, "login password incorrect") return } s.store.Event(p.ID, source(r), "secret_viewed", body.Field) respond(w, 200, map[string]string{"value": value}) }