112 lines
5.1 KiB
Go
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
|
|
}
|