server läuft
This commit is contained in:
1 parent
c8df9d7651
commit
42f0c0accb
21 files changed
+4185
-10
No files matched your search
@@ -0,0 +1,162 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"sultan/pkg/auth"
|
||||
"sultan/pkg/policy"
|
||||
"sultan/pkg/registry"
|
||||
"sultan/pkg/store"
|
||||
)
|
||||
|
||||
// EnrollRequest ist der Body von POST /api/v1/enroll. Der Request ist mit
|
||||
// dem neuen Schlüssel des Agenten signiert (Besitznachweis).
|
||||
type EnrollRequest struct {
|
||||
Token string `json:"token"` // sltn_enroll_<secret>, ohne Fingerprint
|
||||
NodeID string `json:"node_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// EnrollResponse ist die Antwort auf ein erfolgreiches Enrollment.
|
||||
type EnrollResponse struct {
|
||||
NodeID string `json:"node_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
MasterPublicKey string `json:"master_public_key"` // zum Prüfen von Lizenzen/Leases
|
||||
}
|
||||
|
||||
func (s *Server) agentRoutes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /api/v1/enroll", s.handleEnroll)
|
||||
mux.HandleFunc("GET /api/v1/stream", s.handleStream)
|
||||
mux.HandleFunc("POST /api/v1/state", s.handleState)
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) providerRoutes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /v1/traefik-config", s.registry.Handler())
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte("ok\n"))
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) handleEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
pub, err := s.verifier.VerifyUnregistered(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
return
|
||||
}
|
||||
var req EnrollRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "ungültiger Body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !policy.ValidID(req.NodeID) || strings.TrimSpace(req.Hostname) == "" {
|
||||
http.Error(w, "node_id ungültig oder hostname fehlt", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
node, err := s.store.Enroll(r.Context(), req.Token, store.Node{
|
||||
ID: req.NodeID,
|
||||
Hostname: req.Hostname,
|
||||
PublicKey: auth.EncodePublicKey(pub),
|
||||
IPAddress: remoteIP(r),
|
||||
Status: store.NodeOffline, // online erst mit offenem Stream
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, store.ErrNotFound), errors.Is(err, store.ErrTokenUsed), errors.Is(err, store.ErrTokenExpired):
|
||||
// Keine Details nach außen, ob Token unbekannt, verbraucht oder abgelaufen.
|
||||
s.log.Warn("Enrollment abgelehnt", "node", req.NodeID, "ip", remoteIP(r), "err", err)
|
||||
http.Error(w, "Einladung ungültig", http.StatusForbidden)
|
||||
return
|
||||
case errors.Is(err, store.ErrExists):
|
||||
http.Error(w, "Knoten-ID oder Schlüssel bereits registriert", http.StatusConflict)
|
||||
return
|
||||
case errors.Is(err, store.ErrInvalid):
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
case err != nil:
|
||||
s.log.Error("Enrollment", "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.log.Info("Knoten registriert", "node", node.ID, "tenant", node.TenantID, "ip", node.IPAddress)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(EnrollResponse{
|
||||
NodeID: node.ID,
|
||||
TenantID: node.TenantID,
|
||||
MasterPublicKey: auth.EncodePublicKey(s.MasterPublicKey()),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := s.verifier.Verify(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
return
|
||||
}
|
||||
if err := s.store.TouchNode(r.Context(), id.Node.ID, remoteIP(r)); err != nil {
|
||||
s.log.Error("Lebenszeichen speichern", "node", id.Node.ID, "err", err)
|
||||
}
|
||||
err = s.hub.Serve(w, r, id.Node.ID)
|
||||
s.log.Debug("Stream beendet", "node", id.Node.ID, "reason", err)
|
||||
}
|
||||
|
||||
func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := s.verifier.Verify(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
return
|
||||
}
|
||||
// Lebendigkeit hängt am Stream: Ohne offenen Stream würden die Routen
|
||||
// beim nächsten Trennen nie wieder entfernt.
|
||||
if !s.hub.Connected(id.Node.ID) {
|
||||
http.Error(w, "Stream nicht verbunden", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
var state registry.NodeState
|
||||
if err := json.NewDecoder(r.Body).Decode(&state); err != nil {
|
||||
http.Error(w, "ungültiger Body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = s.registry.Update(r.Context(), id.Node.ID, state)
|
||||
switch {
|
||||
case errors.Is(err, registry.ErrUnknownNode):
|
||||
http.Error(w, "Knoten unbekannt", http.StatusUnauthorized)
|
||||
return
|
||||
case errors.Is(err, policy.ErrInvalidInput):
|
||||
s.log.Error("Zustand abgelehnt", "node", id.Node.ID, "err", err)
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
case err != nil:
|
||||
s.log.Error("Zustand übernehmen", "node", id.Node.ID, "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := s.store.TouchNode(r.Context(), id.Node.ID, remoteIP(r)); err != nil {
|
||||
s.log.Error("Lebenszeichen speichern", "node", id.Node.ID, "err", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func authError(w http.ResponseWriter, err error) {
|
||||
code := auth.StatusCode(err)
|
||||
msg := http.StatusText(code)
|
||||
if code == http.StatusUpgradeRequired {
|
||||
msg = "nur über TLS"
|
||||
}
|
||||
http.Error(w, msg, code)
|
||||
}
|
||||
|
||||
func remoteIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
Reference in new issue
Block a user