142 lines
5.9 KiB
Go
142 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"log"
|
|
"maps"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
"git.ipao.vip/rogee/go-sip/internal/agent"
|
|
"git.ipao.vip/rogee/go-sip/internal/config"
|
|
"git.ipao.vip/rogee/go-sip/internal/rpc"
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// newCurrentAgentServer binds the authenticated Agent session, task controls
|
|
// and per-call Mock recording delivery. Serving the returned server requires
|
|
// a separately verified mutual-TLS listener and a pinned local D connection.
|
|
func newCurrentAgentServer(ctx context.Context, settings config.AgentEnvironment, scenario approvedMockScenario, appliedSIP map[string]int64, dispatcher agentpb.AgentControlServiceClient) (*rpc.Server, error) {
|
|
if ctx == nil || ctx.Err() != nil || settings.AgentID == "" || settings.CellID == "" || settings.SessionPath == "" ||
|
|
settings.RecoveryRoot == "" || len(settings.PeerFingerprints) == 0 || len(appliedSIP) == 0 ||
|
|
scenario.MaxWAVBytes <= 44 || len(scenario.Script.Turns) == 0 || strings.TrimSpace(scenario.ReasonMessage) == "" {
|
|
return nil, errors.New("current Agent requires an active process, explicit Mock media and deployment identity")
|
|
}
|
|
if tenant.ValidateDispatcherID(settings.DispatcherID) != nil {
|
|
return nil, errors.New("current Agent requires an approved Dispatcher UUID v4")
|
|
}
|
|
if dispatcher == nil {
|
|
return nil, errors.New("current Agent requires a pinned Dispatcher transport")
|
|
}
|
|
value := reflect.ValueOf(dispatcher)
|
|
switch value.Kind() {
|
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
|
if value.IsNil() {
|
|
return nil, errors.New("current Agent requires a pinned Dispatcher transport")
|
|
}
|
|
}
|
|
root, err := os.Stat(settings.RecoveryRoot)
|
|
if err != nil || !root.IsDir() || root.Mode().Perm() != 0700 {
|
|
return nil, errors.New("current Agent requires an existing private 0700 recovery directory")
|
|
}
|
|
for trunk, revision := range appliedSIP {
|
|
if strings.TrimSpace(trunk) == "" || revision <= 0 {
|
|
return nil, errors.New("current Agent requires explicit applied Mock SIP revisions")
|
|
}
|
|
}
|
|
loaded := maps.Clone(appliedSIP)
|
|
pins := maps.Clone(settings.PeerFingerprints)
|
|
scenario.InboundPCM16 = bytes.Clone(scenario.InboundPCM16)
|
|
scenario.Script.OpeningPCM16 = bytes.Clone(scenario.Script.OpeningPCM16)
|
|
scenario.Script.Turns = slices.Clone(scenario.Script.Turns)
|
|
for index := range scenario.Script.Turns {
|
|
scenario.Script.Turns[index].ReplyPCM16 = bytes.Clone(scenario.Script.Turns[index].ReplyPCM16)
|
|
}
|
|
ca, err := readAgentPEM("MTLS_CA_FILE", settings.CAFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uploadHTTP, err := localMockHTTPClient(ca)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var handler *rpc.Server
|
|
worker := &rpc.ApprovedCallWorker{
|
|
Lifecycle: ctx,
|
|
Calls: &agent.TaskCalls{},
|
|
Prepare: func(execution rpc.ApprovedExecution) (func(context.Context) error, error) {
|
|
if handler == nil {
|
|
return nil, errors.New("Agent session is unavailable")
|
|
}
|
|
delivery := &agent.RecordingDelivery{
|
|
Call: agent.RecordingClient{
|
|
Client: dispatcher, DispatcherID: execution.DispatcherID, TenantID: execution.TenantID,
|
|
SourceEventID: execution.SourceEventID, Session: func(context.Context) (*agentpb.RequestMeta, error) { return handler.ActiveSessionMeta() },
|
|
},
|
|
Recovery: &agent.RecordingRecovery{
|
|
Root: settings.RecoveryRoot,
|
|
Upload: agent.UploadClient{HTTPClient: uploadHTTP, AllowInsecureHTTP: true},
|
|
},
|
|
}
|
|
mock := &rpc.ApprovedRecordedMockCall{
|
|
InboundPCM16: scenario.InboundPCM16, Script: scenario.Script,
|
|
MaxWAVBytes: scenario.MaxWAVBytes, ExpectedRecording: scenario.ExpectedRecording,
|
|
Outcome: scenario.Outcome, ReasonMessage: scenario.ReasonMessage,
|
|
ReportTimeout: 15 * time.Minute, Delivery: delivery,
|
|
}
|
|
return mock.Prepare(execution)
|
|
},
|
|
OnFailure: func(execution rpc.ApprovedExecution, cause error) error {
|
|
// The runner already tried to report termination and persisted any
|
|
// failed upload. Never invent a second result or retry an unknown PUT.
|
|
log.Printf("Agent Mock call requires inspection: event_id=%q task_id=%q cause_type=%T", execution.SourceEventID, execution.TaskID, cause)
|
|
return nil
|
|
},
|
|
}
|
|
handler, err = rpc.NewApprovedAgentServer(rpc.ServerOptions{
|
|
Mode: "mock", StatePath: settings.SessionPath, ApprovedDispatcherID: settings.DispatcherID,
|
|
Status: &agentpb.AgentStatus{AgentId: settings.AgentID, CellId: settings.CellID, BootId: uuid.NewString(), ProtocolVersion: "agent.v1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) { return maps.Clone(loaded), nil },
|
|
PeerCertificateFingerprints: pins, RequirePeerCertificate: true,
|
|
}, worker)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handler, nil
|
|
}
|
|
|
|
// The isolated Mock uploader may reach localhost only, even if a grant or
|
|
// redirect unexpectedly names a real OSS endpoint. It never logs signed URLs.
|
|
func localMockHTTPClient(trustPEM []byte) (*http.Client, error) {
|
|
roots := x509.NewCertPool()
|
|
if !roots.AppendCertsFromPEM(trustPEM) {
|
|
return nil, errors.New("Mock HTTPS requires an approved trust bundle")
|
|
}
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.Proxy = nil
|
|
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots}
|
|
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, errors.New("Mock upload target is not local")
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if !strings.EqualFold(host, "localhost") && (ip == nil || !ip.IsLoopback()) {
|
|
return nil, errors.New("Mock upload target is not local")
|
|
}
|
|
return (&net.Dialer{}).DialContext(ctx, network, address)
|
|
}
|
|
return &http.Client{Transport: transport, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, nil
|
|
}
|