111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
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})
|
|
}
|