Files
agent-call/management/backend/server.go
T
rogee b6d0af1a56
management-images / build-and-publish (push) Successful in 9m15s
docs: add one-click management deployment and image workflow
2026-09-16 17:57:05 +08:00

777 lines
27 KiB
Go

package management
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
fiberCors "github.com/gofiber/fiber/v3/middleware/cors"
"github.com/sirupsen/logrus"
)
type principal struct {
Token string
TokenSpec
}
type Server struct {
cfg Config
store *Store
app *fiber.App
log *logrus.Logger
}
func NewServer(cfg Config, store *Store) *Server {
if store == nil {
panic("management: nil store")
}
logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{})
s := &Server{cfg: cfg, store: store, log: logger}
s.app = fiber.New(fiber.Config{BodyLimit: 1 << 20, ErrorHandler: s.errorHandler})
s.app.Use(s.originGuard)
s.app.Use(fiberCors.New(fiberCors.Config{AllowOriginsFunc: s.originAllowed, AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodOptions}, AllowHeaders: []string{"Authorization", "Content-Type", "If-Match", "X-Request-ID"}, AllowCredentials: true}))
s.app.Use(func(c fiber.Ctx) error {
started := time.Now()
err := c.Next()
s.log.WithFields(logrus.Fields{"method": c.Method(), "path": c.Path(), "status": c.Response().StatusCode(), "duration_ms": time.Since(started).Milliseconds(), "request_id": c.Get("X-Request-ID")}).Debug("management request")
return err
})
s.registerRoutes()
if err := s.Recover(context.Background()); err != nil {
s.log.WithError(err).Warn("operation recovery requires operator reconciliation")
}
return s
}
func (s *Server) App() *fiber.App { return s.app }
func (s *Server) Listen() error { return s.app.Listen(s.cfg.Addr) }
func (s *Server) errorHandler(c fiber.Ctx, err error) error {
if appErr, ok := err.(*AppError); ok {
return s.fail(c, appErr)
}
return s.fail(c, newAppError(http.StatusInternalServerError, "INTERNAL_ERROR", "internal server error", nil))
}
func (s *Server) registerRoutes() {
s.app.Get("/healthz/live", s.live)
s.app.Get("/admin/v1/providers", s.listProviders)
s.app.Get("/admin/v1/providers/:provider_id", s.getProvider)
s.app.Put("/admin/v1/providers/:provider_id", s.putProvider)
s.app.Get("/admin/v1/trunks", s.listTrunks)
s.app.Get("/admin/v1/trunks/:trunk_id", s.getTrunk)
s.app.Put("/admin/v1/trunks/:trunk_id", s.putTrunk)
s.app.Post("/admin/v1/trunks/:trunk_id/validate", s.validateTrunk)
s.app.Post("/admin/v1/trunks/:trunk_id/verifications", s.addVerification)
s.app.Post("/admin/v1/trunks/:trunk_id/publish", s.publishTrunk)
s.app.Post("/admin/v1/trunks/:trunk_id/disable", s.disableTrunk)
s.app.Post("/admin/v1/trunks/:trunk_id/rollback", s.rollbackTrunk)
s.app.Get("/admin/v1/trunks/:trunk_id/publications", s.listPublications)
s.app.Get("/admin/v1/trunks/:trunk_id/audit", s.listTrunkAudit)
s.app.Get("/admin/v1/cells", s.listCells)
s.app.Get("/admin/v1/cells/:cell_id", s.getCell)
s.app.Put("/admin/v1/cells/:cell_id", s.putCell)
s.app.Post("/admin/v1/cells/:cell_id/observations", s.ingestObservation)
s.app.Get("/admin/v1/egress-pools", s.listEgressPools)
s.app.Get("/admin/v1/sip-status", s.sipStatus)
s.app.Get("/admin/v1/cells/:cell_id/sip-status", s.cellSIPStatus)
s.app.Get("/admin/v1/providers/:provider_id/status", s.providerStatus)
s.app.Get("/admin/v1/statistics/outbound/summary", s.outboundSummary)
s.app.Get("/admin/v1/statistics/outbound/timeseries", s.outboundTimeseries)
s.app.Get("/admin/v1/call-attempts", s.callAttempts)
s.app.Get("/admin/v1/call-attempts/:attempt_id", s.callAttempt)
s.app.Get("/admin/v1/audit", s.audit)
s.app.Get("/admin/v1/operations/:operation_id", s.getOperation)
s.app.Get("/readonly/v1/sip/trunks", s.listReadonlyTrunks)
s.app.Get("/readonly/v1/sip/trunks/:trunk_id", s.getReadonlyTrunk)
s.app.Use(func(c fiber.Ctx) error {
return s.fail(c, newAppError(http.StatusNotFound, "NOT_FOUND", "resource not found", nil))
})
}
func (s *Server) originAllowed(origin string) bool {
if origin == "" || origin == "*" {
return false
}
allowed := s.cfg.AllowedOrigins
if len(allowed) == 0 {
allowed = DefaultConfig().AllowedOrigins
}
return allowed[origin] && origin != "*"
}
func (s *Server) originGuard(c fiber.Ctx) error {
origin := strings.TrimSpace(c.Get("Origin"))
if origin != "" && !s.originAllowed(origin) {
return s.fail(c, newAppError(http.StatusForbidden, "ORIGIN_FORBIDDEN", "browser origin is not allowlisted", nil))
}
return c.Next()
}
func (s *Server) live(c fiber.Ctx) error {
return c.JSON(map[string]any{"status": "ok", "mode": s.cfg.PublicMode()})
}
func (s *Server) authorize(c fiber.Ctx, realm, scope, resourceType, resourceID string) (principal, *AppError) {
header := strings.TrimSpace(c.Get("Authorization"))
if !strings.HasPrefix(header, "Bearer ") || strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) == "" {
return principal{}, newAppError(http.StatusUnauthorized, "UNAUTHORIZED", "a bearer token is required", nil)
}
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
spec, ok := s.cfg.Tokens[token]
if !ok || spec.Realm != realm {
return principal{}, newAppError(http.StatusUnauthorized, "UNAUTHORIZED", "token is not valid for this API", nil)
}
if !containsString(spec.Scopes, scope) {
return principal{}, newAppError(http.StatusForbidden, "FORBIDDEN", "credential lacks the required scope", map[string]any{"required_scope": scope})
}
if resourceID != "" && !resourceAllowed(spec, resourceType, resourceID) {
return principal{}, newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "credential is not authorized for this resource", map[string]any{"resource_type": resourceType, "resource_id": resourceID})
}
return principal{Token: token, TokenSpec: spec}, nil
}
func resourceAllowed(spec TokenSpec, resourceType, resourceID string) bool {
allowed, ok := spec.Resources[resourceType]
if !ok || len(allowed) == 0 {
return true
}
return containsString(allowed, "*") || containsString(allowed, resourceID)
}
func (s *Server) checkFilterResources(c fiber.Ctx, p principal) *AppError {
for _, id := range parseCSV(c.Query("cell_ids")) {
if !resourceAllowed(p.TokenSpec, "cell", id) {
return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "one or more requested Cells are outside the authorized scope", map[string]any{"cell_id": id})
}
}
for _, id := range parseCSV(c.Query("cell_id")) {
if !resourceAllowed(p.TokenSpec, "cell", id) {
return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested Cell is outside the authorized scope", map[string]any{"cell_id": id})
}
}
if id := c.Query("provider_id"); id != "" && !resourceAllowed(p.TokenSpec, "provider", id) {
return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested provider is outside the authorized scope", map[string]any{"provider_id": id})
}
if id := c.Query("trunk_id"); id != "" && !resourceAllowed(p.TokenSpec, "trunk", id) {
return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested trunk is outside the authorized scope", map[string]any{"trunk_id": id})
}
if id := c.Query("egress_pool_id"); id != "" && !resourceAllowed(p.TokenSpec, "egress_pool", id) {
return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested egress pool is outside the authorized scope", map[string]any{"egress_pool_id": id})
}
return nil
}
func (s *Server) fail(c fiber.Ctx, err *AppError) error {
if err == nil {
err = newAppError(http.StatusInternalServerError, "INTERNAL_ERROR", "internal server error", nil)
}
requestID := strings.TrimSpace(c.Get("X-Request-ID"))
body := errorResponse{Error: ErrorBody{Code: err.Code, Message: err.Message, Fields: err.Fields, RequestID: requestID}}
return c.Status(err.Status).JSON(body)
}
func (s *Server) context(_ fiber.Ctx) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 10*time.Second)
}
func requireHeader(c fiber.Ctx, name string) (string, *AppError) {
value := strings.TrimSpace(c.Get(name))
if value == "" {
status := http.StatusPreconditionRequired
code := "PRECONDITION_REQUIRED"
if name == "X-Request-ID" {
status = http.StatusBadRequest
code = "REQUEST_ID_REQUIRED"
}
return "", newAppError(status, code, name+" header is required", nil)
}
if name == "X-Request-ID" && !validRequestID(value) {
return "", newAppError(http.StatusBadRequest, "INVALID_REQUEST_ID", "X-Request-ID is invalid", nil)
}
return strings.Trim(value, `"`), nil
}
func parseIfMatch(c fiber.Ctx) (int64, *AppError) {
value, err := requireHeader(c, "If-Match")
if err != nil {
return 0, err
}
value = strings.TrimSpace(strings.Trim(value, `"`))
if value == "" || value == "*" {
return 0, newAppError(http.StatusPreconditionRequired, "IF_MATCH_REQUIRED", "If-Match must contain an exact revision", nil)
}
revision, parseErr := strconv.ParseInt(value, 10, 64)
if parseErr != nil || revision < 0 {
return 0, newAppError(http.StatusBadRequest, "INVALID_IF_MATCH", "If-Match must be a non-negative revision", nil)
}
return revision, nil
}
func decodeJSON(c fiber.Ctx, dst any) *AppError {
body := c.Body()
if len(body) == 0 {
return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body is required", nil)
}
decoder := json.NewDecoder(strings.NewReader(string(body)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(dst); err != nil {
return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body is invalid", nil)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body must contain one JSON value", nil)
}
return nil
}
func writeStored(c fiber.Ctx, status int, raw string) error {
c.Status(status)
c.Set("Content-Type", "application/json; charset=utf-8")
return c.SendString(raw)
}
func (s *Server) beginOperation(ctx context.Context, p principal, action, resourceType, resourceID, requestID string, payload any, expected int64) (Operation, bool, *AppError) {
digest, _, err := hashJSON(payload)
if err != nil {
return Operation{}, false, newAppError(500, "INTERNAL_ERROR", "could not create request digest", nil)
}
op, replay, err := s.store.CreateOperation(ctx, p.Principal, action, resourceType, resourceID, requestID, digest, expected)
if err != nil {
return Operation{}, false, asAppError(err)
}
if !replay {
return op, false, nil
}
if op.RequestDigest != digest {
return Operation{}, false, newAppError(http.StatusConflict, "IDEMPOTENCY_CONFLICT", "the same request ID was used with a different payload", map[string]any{"operation_id": op.OperationID})
}
if op.State == "succeeded" && op.ResponseJSON != "" {
return op, true, nil
}
if (op.State == "failed" || op.State == "pending" || op.State == "needs_reconciliation") && op.ErrorJSON != "" {
return op, true, nil
}
return op, true, newAppError(http.StatusConflict, "OPERATION_IN_PROGRESS", "the operation is still in progress; query it by operation_id", map[string]any{"operation_id": op.OperationID})
}
func (s *Server) replayOrError(c fiber.Ctx, op Operation, beginErr *AppError) error {
if beginErr != nil {
return s.fail(c, beginErr)
}
if op.State == "succeeded" && op.ResponseJSON != "" {
return writeStored(c, op.HTTPStatus, op.ResponseJSON)
}
if op.ErrorJSON != "" {
return writeStored(c, op.HTTPStatus, op.ErrorJSON)
}
return s.fail(c, newAppError(http.StatusConflict, "OPERATION_IN_PROGRESS", "the operation is still in progress; query it by operation_id", map[string]any{"operation_id": op.OperationID}))
}
func (s *Server) finishSuccess(ctx context.Context, op Operation, status int, value any) error {
body, err := json.Marshal(value)
if err != nil {
return err
}
return s.store.FinishOperation(ctx, op.OperationID, "succeeded", status, string(body), "")
}
func (s *Server) finishError(ctx context.Context, op Operation, appErr *AppError, c fiber.Ctx) error {
requestID := strings.TrimSpace(c.Get("X-Request-ID"))
body, _ := json.Marshal(errorResponse{Error: ErrorBody{Code: appErr.Code, Message: appErr.Message, Fields: appErr.Fields, RequestID: requestID}})
state := "failed"
if appErr.Code == "PUBLISH_PENDING" || appErr.Code == "PUBLISH_PARTIAL" || appErr.Code == "OPERATION_IN_PROGRESS" {
state = "pending"
}
if err := s.store.FinishOperation(ctx, op.OperationID, state, appErr.Status, "", string(body)); err != nil {
return err
}
return s.fail(c, appErr)
}
func (s *Server) listProviders(c fiber.Ctx) error {
p, authErr := s.authorize(c, "admin", "sip.provider.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
providers, err := s.store.ListProviders(ctx)
if err != nil {
return s.fail(c, asAppError(err))
}
visible := providers[:0]
for _, provider := range providers {
if resourceAllowed(p.TokenSpec, "provider", provider.ProviderID) {
visible = append(visible, provider)
}
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "providers": visible})
}
func (s *Server) getProvider(c fiber.Ctx) error {
id := c.Params("provider_id")
p, authErr := s.authorize(c, "admin", "sip.provider.read", "provider", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
provider, err := s.store.GetProvider(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "PROVIDER_NOT_FOUND", "provider does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "provider": provider})
}
func (s *Server) putProvider(c fiber.Ctx) error {
id := c.Params("provider_id")
p, authErr := s.authorize(c, "admin", "sip.provider.write", "provider", id)
if authErr != nil {
return s.fail(c, authErr)
}
expected, err := parseIfMatch(c)
if err != nil {
return s.fail(c, err)
}
requestID, err := requireHeader(c, "X-Request-ID")
if err != nil {
return s.fail(c, err)
}
var input ProviderInput
if err := decodeJSON(c, &input); err != nil {
return s.fail(c, err)
}
ctx, cancel := s.context(c)
defer cancel()
op, replay, beginErr := s.beginOperation(ctx, p, "provider.upsert", "provider", id, requestID, map[string]any{"if_match": expected, "input": input}, expected)
if replay || beginErr != nil {
return s.replayOrError(c, op, beginErr)
}
provider, status, opErr := s.store.UpsertProvider(ctx, id, input, expected, p.Principal, requestID)
if opErr != nil {
appErr := asAppError(opErr)
_ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID))
return s.fail(c, appErr)
}
body := map[string]any{"mode": s.cfg.PublicMode(), "provider": provider}
if err := s.finishSuccess(ctx, op, status, body); err != nil {
return s.fail(c, asAppError(err))
}
return c.Status(status).JSON(body)
}
func errorJSON(err *AppError, requestID string) string {
b, _ := json.Marshal(errorResponse{Error: ErrorBody{Code: err.Code, Message: err.Message, Fields: err.Fields, RequestID: requestID}})
return string(b)
}
func (s *Server) listTrunks(c fiber.Ctx) error {
p, authErr := s.authorize(c, "admin", "sip.trunk.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
if err := s.checkFilterResources(c, p); err != nil {
return s.fail(c, err)
}
ctx, cancel := s.context(c)
defer cancel()
trunks, err := s.store.ListTrunks(ctx, s.cfg.PublicMode(), c.Query("provider_id"))
if err != nil {
return s.fail(c, asAppError(err))
}
if cellID := c.Query("cell_id"); cellID != "" {
filtered := trunks[:0]
for _, trunk := range trunks {
if containsString(trunk.CompatibleCellIDs, cellID) {
filtered = append(filtered, trunk)
}
}
trunks = filtered
}
if state := c.Query("status"); state != "" {
filtered := trunks[:0]
for _, trunk := range trunks {
if trunk.Status == state || trunk.ActiveStatus == state {
filtered = append(filtered, trunk)
}
}
trunks = filtered
}
visible := trunks[:0]
for _, trunk := range trunks {
if resourceAllowed(p.TokenSpec, "trunk", trunk.TrunkID) && resourceAllowed(p.TokenSpec, "provider", trunk.ProviderID) {
visible = append(visible, trunk)
}
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "trunks": visible})
}
func (s *Server) getTrunk(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.trunk.read", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode())
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(trunk)
}
func (s *Server) putTrunk(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
expected, err := parseIfMatch(c)
if err != nil {
return s.fail(c, err)
}
requestID, err := requireHeader(c, "X-Request-ID")
if err != nil {
return s.fail(c, err)
}
var input TrunkConfig
if err := decodeJSON(c, &input); err != nil {
return s.fail(c, err)
}
ctx, cancel := s.context(c)
defer cancel()
op, replay, beginErr := s.beginOperation(ctx, p, "trunk.upsert", "trunk", id, requestID, map[string]any{"if_match": expected, "input": input}, expected)
if replay || beginErr != nil {
return s.replayOrError(c, op, beginErr)
}
trunk, status, opErr := s.store.PutTrunk(ctx, id, input, expected, p.Principal, requestID)
if opErr != nil {
appErr := asAppError(opErr)
_ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID))
return s.fail(c, appErr)
}
trunk.Mode = s.cfg.PublicMode()
body := map[string]any{"mode": s.cfg.PublicMode(), "trunk": trunk}
if err := s.finishSuccess(ctx, op, status, body); err != nil {
return s.fail(c, asAppError(err))
}
return c.Status(status).JSON(body)
}
func (s *Server) validateTrunk(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
var body struct {
Revision int64 `json:"revision"`
}
if len(c.Body()) > 0 {
if err := decodeJSON(c, &body); err != nil {
return s.fail(c, err)
}
}
ctx, cancel := s.context(c)
defer cancel()
if body.Revision == 0 {
trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode())
if err != nil {
return s.fail(c, asAppError(err))
}
body.Revision = trunk.LatestRevision
}
result, err := s.store.BuildValidation(ctx, id, body.Revision, s.cfg.PublicMode())
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "VERSION_NOT_FOUND", "trunk version does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(result)
}
func (s *Server) listCells(c fiber.Ctx) error {
p, authErr := s.authorize(c, "admin", "sip.cell.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
cells, err := s.store.ListCells(ctx)
if err != nil {
return s.fail(c, asAppError(err))
}
visible := cells[:0]
for i := range cells {
if resourceAllowed(p.TokenSpec, "cell", cells[i].CellID) {
cells[i].Mode = s.cfg.PublicMode()
visible = append(visible, cells[i])
}
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "cells": visible})
}
func (s *Server) getCell(c fiber.Ctx) error {
id := c.Params("cell_id")
p, authErr := s.authorize(c, "admin", "sip.cell.read", "cell", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
cell, err := s.store.GetCell(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "CELL_NOT_FOUND", "cell does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
cell.Mode = s.cfg.PublicMode()
return c.JSON(cell)
}
func (s *Server) putCell(c fiber.Ctx) error {
id := c.Params("cell_id")
p, authErr := s.authorize(c, "admin", "sip.cell.write", "cell", id)
if authErr != nil {
return s.fail(c, authErr)
}
expected, err := parseIfMatch(c)
if err != nil {
return s.fail(c, err)
}
requestID, err := requireHeader(c, "X-Request-ID")
if err != nil {
return s.fail(c, err)
}
var input struct {
CellConfig
CloudInstanceID string `json:"cloud_instance_id,omitempty"`
InstanceName string `json:"instance_name,omitempty"`
Region string `json:"region,omitempty"`
}
if err := decodeJSON(c, &input); err != nil {
return s.fail(c, err)
}
if input.ManagementURL != "" {
allowPrivate := s.cfg.AllowPrivateAgent
allowlist := s.cfg.AgentAllowlist
if s.cfg.Mode != ModeReal {
allowPrivate = true
allowlist = nil
}
if err := validateManagementURL(input.ManagementURL, allowPrivate, allowlist); err != nil {
return s.fail(c, newAppError(422, "INVALID_MANAGEMENT_URL", err.Error(), nil))
}
}
ctx, cancel := s.context(c)
defer cancel()
op, replay, beginErr := s.beginOperation(ctx, p, "cell.upsert", "cell", id, requestID, map[string]any{"if_match": expected, "input": input}, expected)
if replay || beginErr != nil {
return s.replayOrError(c, op, beginErr)
}
cell, status, opErr := s.store.UpsertCell(ctx, id, input.CellConfig, expected, p.Principal, requestID, input.CloudInstanceID, input.InstanceName, input.Region)
if opErr != nil {
appErr := asAppError(opErr)
_ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID))
return s.fail(c, appErr)
}
cell.Mode = s.cfg.PublicMode()
body := map[string]any{"mode": s.cfg.PublicMode(), "cell": cell}
if err := s.finishSuccess(ctx, op, status, body); err != nil {
return s.fail(c, asAppError(err))
}
return c.Status(status).JSON(body)
}
func (s *Server) listEgressPools(c fiber.Ctx) error {
p, authErr := s.authorize(c, "admin", "sip.cell.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
pools, err := s.store.ListEgressPools(ctx)
if err != nil {
return s.fail(c, asAppError(err))
}
visible := pools[:0]
for _, pool := range pools {
if resourceAllowed(p.TokenSpec, "egress_pool", fmt.Sprint(pool["egress_pool_id"])) {
visible = append(visible, pool)
}
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "egress_pools": visible})
}
func (s *Server) listPublications(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.trunk.read", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
rows, err := s.store.GetPublicationRows(ctx, id, nil)
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "publications": rows})
}
func (s *Server) listTrunkAudit(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.audit.read", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
rows, err := s.store.ListAudit(ctx, id, "", "", 200)
if err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "audit": rows})
}
func (s *Server) getOperation(c fiber.Ctx) error {
id := c.Params("operation_id")
p, authErr := s.authorize(c, "admin", "sip.operation.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
op, err := s.store.GetOperation(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "OPERATION_NOT_FOUND", "operation does not exist", nil))
}
if err != nil {
return s.fail(c, asAppError(err))
}
if !resourceAllowed(p.TokenSpec, op.ResourceType, op.ResourceID) {
return s.fail(c, newAppError(403, "RESOURCE_FORBIDDEN", "operation is outside the authorized scope", nil))
}
out := map[string]any{"operation_id": op.OperationID, "action": op.Action, "resource_type": op.ResourceType, "resource_id": op.ResourceID, "request_id": op.RequestID, "state": op.State, "http_status": op.HTTPStatus, "created_at": op.CreatedAt, "updated_at": op.UpdatedAt, "expires_at": op.ExpiresAt}
if op.ResponseJSON != "" {
var v any
_ = json.Unmarshal([]byte(op.ResponseJSON), &v)
out["response"] = v
}
if op.ErrorJSON != "" {
var v any
_ = json.Unmarshal([]byte(op.ErrorJSON), &v)
out["error"] = v
}
return c.JSON(out)
}
func (s *Server) listReadonlyTrunks(c fiber.Ctx) error {
p, authErr := s.authorize(c, "saas", "sip.trunk.read", "", "")
if authErr != nil {
return s.fail(c, authErr)
}
ctx, cancel := s.context(c)
defer cancel()
trunks, err := s.store.ListTrunks(ctx, s.cfg.PublicMode(), "")
if err != nil {
return s.fail(c, asAppError(err))
}
out := make([]ReadonlyTrunk, 0, len(trunks))
for _, trunk := range trunks {
if trunk.ActiveRevision == 0 || trunk.ActiveStatus != "published" || !resourceAllowed(p.TokenSpec, "trunk", trunk.TrunkID) || !resourceAllowed(p.TokenSpec, "provider", trunk.ProviderID) {
continue
}
out = append(out, ReadonlyTrunk{Mode: s.cfg.PublicMode(), TrunkID: trunk.TrunkID, ProviderID: trunk.ProviderID, Revision: trunk.ActiveRevision, Status: "published", UpdatedAt: trunk.UpdatedAt, Config: trunk.Active})
}
return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "trunks": out})
}
func (s *Server) getReadonlyTrunk(c fiber.Ctx) error {
id := c.Params("trunk_id")
p, authErr := s.authorize(c, "saas", "sip.trunk.read", "trunk", id)
if authErr != nil {
return s.fail(c, authErr)
}
_ = p
ctx, cancel := s.context(c)
defer cancel()
trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode())
if errors.Is(err, sql.ErrNoRows) || trunk.ActiveRevision == 0 || trunk.ActiveStatus != "published" {
return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "published trunk does not exist", nil))
}
return c.JSON(ReadonlyTrunk{Mode: s.cfg.PublicMode(), TrunkID: id, ProviderID: trunk.ProviderID, Revision: trunk.ActiveRevision, Status: "published", UpdatedAt: trunk.UpdatedAt, Config: trunk.Active})
}
// httpClient is intentionally used only for the real Cell Agent protocol. Mock mode never dials a network address.
func (s *Server) httpClient() (*http.Client, error) {
rootPEM, certFile, keyFile := os.Getenv("MANAGEMENT_AGENT_CA"), os.Getenv("MANAGEMENT_AGENT_CLIENT_CERT"), os.Getenv("MANAGEMENT_AGENT_CLIENT_KEY")
if rootPEM == "" || certFile == "" || keyFile == "" {
return nil, fmt.Errorf("mTLS client certificate is not configured")
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("load mTLS client certificate: %w", err)
}
pool := x509.NewCertPool()
if rootPEM != "" {
pemBytes, err := os.ReadFile(rootPEM)
if err != nil {
return nil, fmt.Errorf("read Cell CA: %w", err)
}
if !pool.AppendCertsFromPEM(pemBytes) {
return nil, fmt.Errorf("invalid Cell CA")
}
}
return &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{cert}, RootCAs: pool, ServerName: ""}}}, nil
}
func validateAgentURL(raw string, cfg Config) (*url.URL, error) {
u, err := url.Parse(raw)
if err != nil {
return nil, err
}
if err := validateManagementURL(raw, cfg.AllowPrivateAgent, cfg.AgentAllowlist); err != nil {
return nil, err
}
return u, nil
}