package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "net" "os" "strings" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "git.ipao.vip/rogee/go-sip/internal/config" "git.ipao.vip/rogee/go-sip/internal/rpc" "github.com/spf13/cobra" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) func newCurrentAgentCommand() *cobra.Command { var mode string command := &cobra.Command{ Use: "agent", Short: "Run the isolated Agent", Args: cobra.NoArgs, SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { settings, err := config.LoadAgentEnvironment(mode) if err != nil { return err } scenario, err := loadApprovedMockScenario(settings.MockScenarioFile) if err != nil { return err } applied, err := loadMockAppliedSIP(settings.MockAppliedSIPFile) if err != nil { return err } ca, err := readAgentPEM("MTLS_CA_FILE", settings.CAFile) if err != nil { return err } cert, err := readAgentPEM("MTLS_CERT_FILE", settings.CertFile) if err != nil { return err } key, err := readAgentPEM("MTLS_KEY_FILE", settings.KeyFile) if err != nil { return err } serverTLS, err := rpc.NewServerTLSConfig(ca, cert, key) if err != nil { return errors.New("Agent mTLS listener certificate is invalid") } clientTLS, err := rpc.NewClientTLSConfig(ca, cert, key, settings.DispatcherServerName) if err != nil { return errors.New("Agent mTLS Dispatcher certificate configuration is invalid") } connection, err := grpc.NewClient(settings.DispatcherEndpoint, grpc.WithTransportCredentials(credentials.NewTLS(clientTLS))) if err != nil { return errors.New("Agent cannot create pinned Dispatcher connection") } defer connection.Close() handler, err := newCurrentAgentServer(cmd.Context(), settings, scenario, applied, agentpb.NewAgentControlServiceClient(connection)) if err != nil { return err } listener, err := net.Listen("tcp", settings.Listen) if err != nil { return fmt.Errorf("Agent cannot listen on configured Mock address: %w", err) } defer listener.Close() server := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLS))) agentpb.RegisterAgentControlServiceServer(server, handler) stopped := make(chan struct{}) go func() { select { case <-cmd.Context().Done(): server.GracefulStop() case <-stopped: } }() err = server.Serve(listener) close(stopped) if err != nil && !errors.Is(err, grpc.ErrServerStopped) { return fmt.Errorf("Agent gRPC listener stopped: %w", err) } if cmd.Context().Err() == nil { return errors.New("Agent gRPC listener stopped without shutdown") } return nil }, } command.Flags().StringVar(&mode, "mode", "mock", "isolated Mock mode only") return command } // This is an isolated deployment fixture, not SaaS SIP configuration or proof // that Asterisk actually loaded the revisions. Real/mixed startup is rejected. func loadMockAppliedSIP(path string) (map[string]int64, error) { if strings.TrimSpace(path) == "" { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE is required") } file, err := os.Open(path) if err != nil { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE cannot be opened") } defer file.Close() data, err := io.ReadAll(io.LimitReader(file, 64<<10+1)) if err != nil || len(data) > 64<<10 { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE exceeds its size limit or cannot be read") } var fixture struct { Trunks map[string]int64 `json:"trunks"` } decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if err := decoder.Decode(&fixture); err != nil { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE has invalid fields") } if err := decoder.Decode(new(any)); err != io.EOF { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE contains extra data") } if len(fixture.Trunks) == 0 { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE requires explicit trunks") } for trunk, revision := range fixture.Trunks { if strings.TrimSpace(trunk) == "" || revision <= 0 { return nil, errors.New("AGENT_MOCK_APPLIED_SIP_FILE has invalid trunk revision") } } return fixture.Trunks, nil } func readAgentPEM(name, path string) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("%s cannot be opened", name) } defer file.Close() data, err := io.ReadAll(io.LimitReader(file, 1<<20+1)) if err != nil || len(data) == 0 || len(data) > 1<<20 { return nil, fmt.Errorf("%s is unreadable or too large", name) } return data, nil }