Files

109 lines
3.0 KiB
Go

package api
import (
"linkport/internal/alerts"
"linkport/internal/gateway"
"linkport/internal/security"
"linkport/internal/store"
"linkport/internal/syncjob"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type session struct {
User, CSRF, Revision string
Expires time.Time
}
type attempt struct {
Count int
Until time.Time
}
type probe struct {
Key, Fingerprint, Host string
Port int
Until time.Time
Session string
}
type Server struct {
alerts *alerts.Engine
syncer *syncjob.Engine
store *store.Store
manager *gateway.Manager
mu sync.Mutex
ops sync.Mutex
sessions map[string]session
attempts map[string]attempt
probes map[string]probe
secure bool
authEnabled bool
devCSRF string
}
func New(s *store.Store, m *gateway.Manager, secure bool, authentication ...bool) *Server {
enabled := true
if len(authentication) > 0 {
enabled = authentication[0]
}
server := &Server{store: s, manager: m, sessions: map[string]session{}, attempts: map[string]attempt{}, probes: map[string]probe{}, secure: secure, authEnabled: enabled, devCSRF: security.Token(), alerts: alerts.New(s, m)}
server.alerts.Start()
m.OnShutdown(server.alerts.Close)
server.syncer = syncjob.New(s, func() func() { server.ops.Lock(); return server.ops.Unlock }, func() bool {
gateways, err := s.List()
if err != nil {
return false
}
for _, p := range gateways {
if m.Status(p.ID).Running {
return false
}
}
return true
})
server.syncer.Start()
m.OnShutdown(server.syncer.Close)
return server
}
func (s *Server) Handler(static http.Handler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /readyz", s.readiness)
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { respond(w, 200, map[string]string{"status": "ok"}) })
mux.HandleFunc("/api/v1/", s.serve)
mux.Handle("/", static)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'")
mux.ServeHTTP(w, r)
})
}
func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
path := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/v1/"), "/")
if r.Method != "GET" {
if origin := r.Header.Get("Origin"); origin != "" {
u, e := url.Parse(origin)
if e != nil || u.Host != r.Host || (u.Scheme != "http" && u.Scheme != "https") {
fail(w, 403, "origin rejected")
return
}
}
}
if path == "login" && r.Method == "POST" {
if !s.authEnabled {
fail(w, 400, "login is disabled in development mode")
return
}
s.login(w, r)
return
}
w, identity, ok := s.authorize(w, r, path)
if !ok {
return
}
s.dispatch(w, r, path, identity)
}