~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
152 lines · 4.5 KB Raw History
package guestcontrolplane

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/guestcp"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/guestidentity"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/guestnet"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/httpjson"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/pinneddownloads"
)

const (
	HealthPath = "/healthz"

	EventsPath = guestnet.EventsPath

	MaxGuestBody = 1 << 20
)

func (s *Service) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /v1/auth/{mount}/{step}", s.handleLogin)
	mux.HandleFunc("GET /v1/identity/oidc/token/{role}", s.handleOIDCToken)
	mux.HandleFunc("GET /v1/identity/oidc/.well-known/openid-configuration", s.handleDiscovery)
	mux.HandleFunc("GET /v1/identity/oidc/.well-known/keys", s.handleKeys)
	mux.HandleFunc("POST "+EventsPath, s.handleEvents)
	pinneddownloads.NewHandler(s.downloads, s.log).Register(mux, DownloadsRoute)
	mux.HandleFunc("GET "+SubscribeEventsPath, s.handleSubscribeEvents)
	mux.HandleFunc("GET "+HealthPath, func(w http.ResponseWriter, r *http.Request) {
		httpjson.Write(w, http.StatusOK, map[string]any{"ok": true})
	})
	return mux
}

func (s *Service) guest() guestcp.Plane {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.plane == nil {
		return nil
	}
	return s.plane
}

func writeReply(w http.ResponseWriter, reply guestcp.Reply) {
	status := reply.Status
	if status < 100 || status > 599 {
		status = http.StatusBadGateway
	}
	body := reply.Body
	if len(body) == 0 {
		body = json.RawMessage(`{}`)
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_, _ = w.Write(body)
}

func writeGuestError(w http.ResponseWriter, status int, message string) {
	httpjson.Write(w, status, map[string]any{"errors": []string{message}})
}

func readJSONMap(r *http.Request) (map[string]any, error) {
	dec := json.NewDecoder(io.LimitReader(r.Body, MaxGuestBody+1))
	value := map[string]any{}
	if err := dec.Decode(&value); err != nil {
		return nil, fmt.Errorf("the body is not a JSON object: %w", err)
	}
	return value, nil
}

func (s *Service) handleLogin(w http.ResponseWriter, r *http.Request) {
	guest := s.guest()
	if guest == nil {
		writeGuestError(w, http.StatusServiceUnavailable, "the guest control plane is not ready")
		return
	}
	mount := r.PathValue("mount")
	if mount != s.spec.GuestLoginMount {
		writeGuestError(w, http.StatusNotFound, "no such mount")
		return
	}
	step := r.PathValue("step")
	if _, err := guestidentity.ParseLoginStep(step); err != nil {
		writeGuestError(w, http.StatusNotFound, "no such step")
		return
	}
	input, err := readJSONMap(r)
	if err != nil {
		writeGuestError(w, http.StatusBadRequest, err.Error())
		return
	}
	writeReply(w, guest.LoginStep(r.Context(), step, input))
}

func (s *Service) handleOIDCToken(w http.ResponseWriter, r *http.Request) {
	guest := s.guest()
	if guest == nil {
		writeGuestError(w, http.StatusServiceUnavailable, "the guest control plane is not ready")
		return
	}
	role := r.PathValue("role")
	if !guestidentity.ValidRole(role) {
		writeGuestError(w, http.StatusNotFound, "no such role")
		return
	}
	writeReply(w, guest.OIDCToken(r.Context(), role, r.Header.Get(guestidentity.VaultTokenHeader)))
}

func (s *Service) handleDiscovery(w http.ResponseWriter, r *http.Request) {
	guest := s.guest()
	if guest == nil {
		writeGuestError(w, http.StatusServiceUnavailable, "the guest control plane is not ready")
		return
	}
	writeReply(w, guest.Discovery(r.Context()))
}

func (s *Service) handleKeys(w http.ResponseWriter, r *http.Request) {
	guest := s.guest()
	if guest == nil {
		writeGuestError(w, http.StatusServiceUnavailable, "the guest control plane is not ready")
		return
	}
	writeReply(w, guest.Keys(r.Context()))
}

func (s *Service) handleEvents(w http.ResponseWriter, r *http.Request) {
	guest := s.guest()
	if guest == nil {
		writeGuestError(w, http.StatusServiceUnavailable, "the guest control plane is not ready")
		return
	}
	var body struct {
		AcceptURI string         `json:"acceptUri"`
		AcceptCID string         `json:"acceptCid"`
		Record    map[string]any `json:"record"`
	}
	dec := json.NewDecoder(io.LimitReader(r.Body, MaxGuestBody+1))
	if err := dec.Decode(&body); err != nil {
		httpjson.Write(w, http.StatusBadRequest, map[string]any{"error": "InvalidRequest"})
		return
	}
	writeReply(w, guest.Events(r.Context(), r.Header.Get("Authorization"), guestcp.EventsRequest{
		AcceptURI: body.AcceptURI,
		AcceptCID: body.AcceptCID,
		Record:    body.Record,
	}))
}