Files
go-sip/cmd/sip-go-agent/agent_real.go
T

112 lines
5.1 KiB
Go

package main
import (
"context"
"errors"
"log"
"os"
"path/filepath"
"reflect"
"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/asterisk"
"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"
)
func reportDefiniteNonDialFailure(ctx context.Context, cause error, client agent.RecordingClient) error {
if !asterisk.NativeCallNeverSubmitted(cause) {
return nil // ARI submission may have happened: keep the unknown reservation
}
endCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
return client.ReportEnded(endCtx)
}
// newRealAgentServer only exposes the signed Dispatcher execution path. There
// is no scenario, synthetic recording, local dial entry, or Mock upload host.
func newRealAgentServer(ctx context.Context, settings config.AgentEnvironment, dispatcher agentpb.AgentControlServiceClient) (*rpc.Server, error) {
if ctx == nil || ctx.Err() != nil || settings.Mode != "nonprod-real" || settings.AgentID == "" || settings.CellID == "" ||
settings.SessionPath == "" || settings.RecoveryRoot == "" || len(settings.PeerFingerprints) == 0 ||
settings.OSSAllowedHost == "" || dispatcher == nil || tenant.ValidateDispatcherID(settings.DispatcherID) != nil {
return nil, errors.New("real Agent requires approved nonproduction identity, pinned Dispatcher and private recovery")
}
value := reflect.ValueOf(dispatcher)
if value.Kind() == reflect.Pointer && value.IsNil() {
return nil, errors.New("real Agent requires a live pinned Dispatcher transport")
}
for _, path := range []string{settings.AsteriskConfigDir, settings.AsteriskBin, settings.AsteriskLibraryDir, settings.EvidenceRoot, settings.RecoveryRoot} {
if !filepath.IsAbs(path) {
return nil, errors.New("real Agent paths must be absolute")
}
}
if strings.ContainsAny(settings.OSSAllowedHost, "/@ ") || settings.MockScenarioFile != "" || settings.MockAppliedSIPFile != "" {
return nil, errors.New("real Agent must not use Mock assets or an invalid OSS host")
}
root, err := os.Stat(settings.RecoveryRoot)
if err != nil || !root.IsDir() || root.Mode().Perm() != 0700 {
return nil, errors.New("real Agent recovery directory must exist with mode 0700")
}
if err := rejectLegacyAgentSpool(settings.RecoveryRoot); err != nil {
return nil, err
}
if _, err := os.Lstat(filepath.Join(settings.RecoveryRoot, ".executions")); err == nil {
return nil, errors.New("legacy Agent execution state requires operator disposition")
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
loader := asterisk.Loader{ConfigDir: settings.AsteriskConfigDir, Asterisk: settings.AsteriskBin, LibraryDir: settings.AsteriskLibraryDir}
var handler *rpc.Server
recordingClient := func(execution rpc.ApprovedExecution) agent.RecordingClient {
return agent.RecordingClient{
Client: dispatcher, DispatcherID: execution.DispatcherID, TenantID: execution.TenantID,
SourceEventID: execution.SourceEventID, Session: func(context.Context) (*agentpb.RequestMeta, error) {
return handler.ActiveSessionMeta()
},
}
}
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: recordingClient(execution),
Recovery: &agent.RecordingRecovery{
Root: settings.RecoveryRoot,
Upload: agent.UploadClient{AllowedHosts: map[string]struct{}{strings.ToLower(settings.OSSAllowedHost): {}}},
},
}
return (&rpc.ApprovedRecordedRealCall{
Loader: loader, MediaPayloadType: 118, EvidenceRoot: settings.EvidenceRoot,
MaxWAVBytes: 64 << 20, ReportTimeout: 15 * time.Minute, Delivery: delivery,
}).Prepare(execution)
},
OnFailure: func(execution rpc.ApprovedExecution, cause error) error {
log.Printf("Agent real call requires inspection: event_id=%q task_id=%q native_phase=%q ari_http_status=%d cause_type=%T", execution.SourceEventID, execution.TaskID, asterisk.NativeCallPhase(cause), asterisk.NativeCallHTTPStatus(cause), cause)
if !asterisk.NativeCallNeverSubmitted(cause) {
return nil
}
if err := reportDefiniteNonDialFailure(ctx, cause, recordingClient(execution)); err != nil {
log.Printf("Agent definite pre-dial end unconfirmed: event_id=%q error_type=%T", execution.SourceEventID, err)
return err
}
log.Printf("Agent definite pre-dial end confirmed: event_id=%q", execution.SourceEventID)
return nil
},
}
handler, err = rpc.NewApprovedAgentServer(rpc.ServerOptions{
Mode: settings.Mode, StatePath: settings.SessionPath, ApprovedDispatcherID: settings.DispatcherID,
Status: &agentpb.AgentStatus{AgentId: settings.AgentID, CellId: settings.CellID, BootId: uuid.NewString(), ProtocolVersion: "agent.v1"},
RequirePeerCertificate: true, PeerCertificateFingerprints: settings.PeerFingerprints,
LoadedSIP: loader.LoadedSIP, ApplySIP: loader.Apply,
}, worker)
return handler, err
}