package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"sourcecraft.dev/bigbes/outline-distro/internal/awgserver"
"sourcecraft.dev/bigbes/outline-distro/internal/config"
"sourcecraft.dev/bigbes/outline-distro/internal/metrics"
"sourcecraft.dev/bigbes/outline-distro/internal/ssserver"
"sourcecraft.dev/bigbes/outline-distro/internal/store"
)
type Handler struct {
store store.Store
ssServer *ssserver.Server
awgServer *awgserver.Server
awgConfig *config.AmneziaWGConfig
tracker *metrics.TransferTracker
version string
logger *slog.Logger
}
func NewHandler(s store.Store, ss *ssserver.Server, awg *awgserver.Server, awgCfg *config.AmneziaWGConfig, tracker *metrics.TransferTracker, version string, logger *slog.Logger) *Handler {
return &Handler{
store: s,
ssServer: ss,
awgServer: awg,
awgConfig: awgCfg,
tracker: tracker,
version: version,
logger: logger,
}
}
// NewRouter creates an HTTP mux with all API routes under the given secret prefix.
func NewRouter(secret string, h *Handler) http.Handler {
mux := http.NewServeMux()
prefix := "/" + secret
// Server endpoints.
mux.HandleFunc("GET "+prefix+"/server", h.getServer)
mux.HandleFunc("PUT "+prefix+"/name", h.renameServer)
// Access key endpoints.
mux.HandleFunc("GET "+prefix+"/access-keys", h.listAccessKeys)
mux.HandleFunc("POST "+prefix+"/access-keys", h.createAccessKey)
mux.HandleFunc("GET "+prefix+"/access-keys/{id}", h.getAccessKey)
mux.HandleFunc("PUT "+prefix+"/access-keys/{id}", h.createAccessKeyWithID)
mux.HandleFunc("DELETE "+prefix+"/access-keys/{id}", h.deleteAccessKey)
mux.HandleFunc("PUT "+prefix+"/access-keys/{id}/name", h.renameAccessKey)
// Data limit endpoints.
mux.HandleFunc("PUT "+prefix+"/access-keys/{id}/data-limit", h.setKeyDataLimit)
mux.HandleFunc("DELETE "+prefix+"/access-keys/{id}/data-limit", h.removeKeyDataLimit)
mux.HandleFunc("PUT "+prefix+"/server/access-key-data-limit", h.setDefaultDataLimit)
mux.HandleFunc("DELETE "+prefix+"/server/access-key-data-limit", h.removeDefaultDataLimit)
// Port and hostname.
mux.HandleFunc("PUT "+prefix+"/server/port-for-new-access-keys", h.setDefaultPort)
mux.HandleFunc("PUT "+prefix+"/server/hostname-for-access-keys", h.setHostname)
// AmneziaWG client config download.
mux.HandleFunc("GET "+prefix+"/access-keys/{id}/awg-config", h.getAWGConfig)
// Metrics endpoints.
mux.HandleFunc("GET "+prefix+"/metrics/enabled", h.getMetricsEnabled)
mux.HandleFunc("PUT "+prefix+"/metrics/enabled", h.setMetricsEnabled)
mux.HandleFunc("GET "+prefix+"/metrics/transfer", h.getTransferMetrics)
return corsMiddleware(mux)
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func (h *Handler) syncKeys() error {
keys := h.store.ListKeys()
if err := h.ssServer.SyncKeys(keys); err != nil {
return err
}
if h.awgServer != nil {
if err := h.awgServer.SyncKeys(keys); err != nil {
return err
}
}
return nil
}
func (h *Handler) accessURL(key store.AccessKey) string {
srv := h.store.GetServer()
hostname := srv.Hostname
if hostname == "" {
hostname = "localhost"
}
// SIP002 URI format: ss://base64(method:password)@host:port#tag
cred := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(
[]byte(fmt.Sprintf("%s:%s", key.Method, key.Password)),
)
return fmt.Sprintf("ss://%s@%s:%d#%s", cred, hostname, key.Port, key.Name)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
http.Error(w, msg, status)
}
func readJSON(r *http.Request, v any) error {
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(v)
}