init
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
"browser.local/run/spool"
|
||||
)
|
||||
|
||||
func TestWorkerExecutesFencedProtectedProgramAndSpoolsDedicatedLogs(t *testing.T) {
|
||||
registry := NewProtectedRequestRegistry()
|
||||
requestText := `{"operation":"status"}`
|
||||
handlerCalled := false
|
||||
if err := registry.Register("program", "scum-program", ProtectedRequestHandlerFunc(func(_ context.Context, request ProtectedRequest) (ProtectedRequestOutcome, error) {
|
||||
handlerCalled = true
|
||||
if request.ServerInstanceID != "server-worker" || request.FencingToken != 12 || request.TargetKey != "scum-program" || request.RequestText != requestText {
|
||||
t.Fatalf("unexpected protected request: %+v", request)
|
||||
}
|
||||
return ProtectedRequestOutcome{Status: ProtectedRequestStatusSucceeded, Stdout: "SCUM ready\npassword=hidden\nconfig /Users/private/scum.ini\ndsn mysql://private", Stderr: "bounded warning"}, nil
|
||||
})); err != nil {
|
||||
t.Fatalf("register protected handler: %v", err)
|
||||
}
|
||||
assignment := protectedWorkerAssignment(protocol.RunCapabilityRemoteRunProgram, "program", "scum-program", 12)
|
||||
client := newFakeWorkerClient()
|
||||
client.claimJob = assignment
|
||||
client.protectedInput = protectedWorkerInput(assignment, "program", requestText)
|
||||
logSpool, err := spool.NewLogSpool(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("log spool: %v", err)
|
||||
}
|
||||
worker, err := NewWorker(workerTestConfig(t), client, WithProtectedRequestRegistry(registry), WithProcessLogSink(&SpoolLogSink{Spool: logSpool}))
|
||||
if err != nil {
|
||||
t.Fatalf("new worker: %v", err)
|
||||
}
|
||||
if err := worker.Register(context.Background()); err != nil {
|
||||
t.Fatalf("register worker: %v", err)
|
||||
}
|
||||
if handled, err := worker.ClaimAndRunOnce(context.Background()); err != nil || !handled {
|
||||
t.Fatalf("claim protected request handled=%v err=%v", handled, err)
|
||||
}
|
||||
if !handlerCalled || len(client.protectedRequests) != 1 || client.protectedRequests[0].FencingToken != assignment.FencingToken {
|
||||
t.Fatalf("expected one fenced protected input read: %+v", client.protectedRequests)
|
||||
}
|
||||
if len(client.resultRequests) != 1 || client.resultRequests[0].State != lifecycleResultStateSucceeded || client.resultRequests[0].ExecutionResult.Kind != "protected.program" {
|
||||
t.Fatalf("unexpected protected result: %+v", client.resultRequests)
|
||||
}
|
||||
batches, err := logSpool.Pending()
|
||||
entryCount := 0
|
||||
for _, batch := range batches {
|
||||
entryCount += len(batch.Entries)
|
||||
}
|
||||
if err != nil || entryCount != 5 {
|
||||
t.Fatalf("expected five program log lines: batches=%+v err=%v", batches, err)
|
||||
}
|
||||
redactedEntries := 0
|
||||
for _, batch := range batches {
|
||||
if batch.Source != "management-program" || batch.Source == "file" || batch.Source == "process" {
|
||||
t.Fatalf("program output used wrong log source: %+v", batch)
|
||||
}
|
||||
for _, entry := range batch.Entries {
|
||||
if entry.Redacted {
|
||||
redactedEntries++
|
||||
}
|
||||
if strings.Contains(entry.Line, "password=hidden") || strings.Contains(entry.Line, "/Users/") || strings.Contains(entry.Line, "://") {
|
||||
t.Fatalf("program log leaked protected output: %+v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
if redactedEntries != 3 {
|
||||
t.Fatalf("expected three explicitly redacted private lines, got %d", redactedEntries)
|
||||
}
|
||||
serialized, err := json.Marshal([]any{client.resultRequests, client.protectedRequests, worker.journal.ActiveJobs()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, private := range []string{requestText, "password=hidden", "/Users/private/scum.ini", "mysql://private", "bounded warning"} {
|
||||
if strings.Contains(string(serialized), private) {
|
||||
t.Fatalf("protected text or output leaked into control projection %q: %s", private, serialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectedRequestUnknownAndBindingFailureAreIsolated(t *testing.T) {
|
||||
registry := NewProtectedRequestRegistry()
|
||||
called := 0
|
||||
if err := registry.Register("rcon", "scum-management", ProtectedRequestHandlerFunc(func(_ context.Context, request ProtectedRequest) (ProtectedRequestOutcome, error) {
|
||||
called++
|
||||
if request.RequestText == "unknown.command" {
|
||||
return ProtectedRequestOutcome{Status: ProtectedRequestStatusUnknown}, ErrProtectedRequestUnknown
|
||||
}
|
||||
return ProtectedRequestOutcome{Status: ProtectedRequestStatusSucceeded}, nil
|
||||
})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(t.TempDir()), WithProtectedRequestRegistry(registry))
|
||||
assignment := protectedWorkerAssignment(protocol.RunCapabilityRemoteRunProtectedRCON, "rcon", "scum-management", 21)
|
||||
unknown := executor.ExecuteProtectedRequest(context.Background(), assignment, protectedWorkerInput(assignment, "rcon", "unknown.command"))
|
||||
if unknown.State != lifecycleResultStateFailed || unknown.ErrorCode != "protected_request_unknown" || unknown.Retryable {
|
||||
t.Fatalf("unexpected unknown outcome: %+v", unknown)
|
||||
}
|
||||
succeeded := executor.ExecuteProtectedRequest(context.Background(), assignment, protectedWorkerInput(assignment, "rcon", "status"))
|
||||
if succeeded.State != lifecycleResultStateSucceeded || called != 2 {
|
||||
t.Fatalf("unknown request affected later request: result=%+v called=%d", succeeded, called)
|
||||
}
|
||||
mismatched := protectedWorkerInput(assignment, "rcon", "status")
|
||||
mismatched.FencingToken++
|
||||
failed := executor.ExecuteProtectedRequest(context.Background(), assignment, mismatched)
|
||||
if failed.ErrorCode != "protected_request_binding_invalid" || called != 2 {
|
||||
t.Fatalf("binding failure reached handler: result=%+v called=%d", failed, called)
|
||||
}
|
||||
encoded, _ := json.Marshal([]LifecycleExecutionResult{unknown, failed})
|
||||
if strings.Contains(string(encoded), "unknown.command") || strings.Contains(string(encoded), "status") {
|
||||
t.Fatalf("safe failure leaked request text: %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectedRCONFallsBackToSourceRCONPlan(t *testing.T) {
|
||||
listener, port := newSourceRCONListener(t)
|
||||
defer listener.Close()
|
||||
password := strings.Repeat("f", 64)
|
||||
assignment := protectedWorkerAssignment(protocol.RunCapabilityRemoteRunProtectedRCON, "rcon", "scum-management", 41)
|
||||
assignment.ExecutionInput.WorkspaceScope = "run-local"
|
||||
assignment.ExecutionInput.TimeoutSeconds = 5
|
||||
assignment.ExecutionInput.SourceRCON = &protocol.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-simple-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/ue4ss/scum-simple-rcon/release.json", Port: port}
|
||||
root := t.TempDir()
|
||||
writeSourceRCONConfig(t, root, assignment, "127.0.0.1", password)
|
||||
commands := make(chan string, 1)
|
||||
serverDone := serveSourceRCONSession(listener, password, func(connection net.Conn, packet sourceRCONPacket) error {
|
||||
commands <- packet.body
|
||||
return writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue})
|
||||
})
|
||||
|
||||
result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithDLLExtensionRuntimeTarget("windows", "amd64")).ExecuteProtectedRequest(context.Background(), assignment, protectedWorkerInput(assignment, "rcon", "#ListPlayers"))
|
||||
if result.State != lifecycleResultStateSucceeded || result.ExecutionResult.Kind != "protected.rcon" {
|
||||
t.Fatalf("expected protected RCON delivery through Source RCON, got %+v", result)
|
||||
}
|
||||
if got := <-commands; got != "#ListPlayers" {
|
||||
t.Fatalf("expected SCUM command delivery, got %q", got)
|
||||
}
|
||||
awaitSourceRCONServer(t, serverDone)
|
||||
serialized, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(serialized), "#ListPlayers") || strings.Contains(string(serialized), password) {
|
||||
t.Fatalf("protected Source RCON result leaked private input: %s", serialized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownProtectedProgramKeepsSafeDiagnosticInProgramLogOnly(t *testing.T) {
|
||||
registry := NewProtectedRequestRegistry()
|
||||
if err := registry.Register("program", "scum-program", ProtectedRequestHandlerFunc(func(context.Context, ProtectedRequest) (ProtectedRequestOutcome, error) {
|
||||
return ProtectedRequestOutcome{Status: ProtectedRequestStatusUnknown, Stderr: "unknown field database=/private/scum.db"}, ErrProtectedRequestUnknown
|
||||
})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sink := &recordingLogSink{}
|
||||
executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(t.TempDir()), WithProtectedRequestRegistry(registry), WithProcessLogSink(sink))
|
||||
assignment := protectedWorkerAssignment(protocol.RunCapabilityRemoteRunProgram, "program", "scum-program", 31)
|
||||
result := executor.ExecuteProtectedRequest(context.Background(), assignment, protectedWorkerInput(assignment, "program", `{"unexpected":true}`))
|
||||
if result.ErrorCode != "protected_request_unknown" || result.State != lifecycleResultStateFailed {
|
||||
t.Fatalf("unexpected unknown program result: %+v", result)
|
||||
}
|
||||
if len(sink.lines) != 1 || !strings.HasPrefix(sink.lines[0], "management-program.stderr:") || strings.Contains(sink.lines[0], "/private/") || !strings.Contains(sink.lines[0], "[redacted protected") {
|
||||
t.Fatalf("unknown program diagnostic was not safely channelized: %+v", sink.lines)
|
||||
}
|
||||
}
|
||||
|
||||
func protectedWorkerAssignment(capability string, kind string, key string, fence uint64) protocol.RunJobAssignment {
|
||||
assignment := workerJobAssignment(capability)
|
||||
assignment.TargetKey = key
|
||||
assignment.InputRef = "input://protected-request/" + assignment.JobID
|
||||
assignment.FencingToken = fence
|
||||
assignment.MaxAttempts = 1
|
||||
assignment.ExecutionInput = protocol.RunJobExecutionInput{RemoteAdapterKey: key, RemoteAdapterKind: "protected-" + kind, TimeoutSeconds: 5}
|
||||
return assignment
|
||||
}
|
||||
|
||||
func protectedWorkerInput(assignment protocol.RunJobAssignment, kind string, text string) protocol.ProtectedRequestExecutionInputResponse {
|
||||
return protocol.ProtectedRequestExecutionInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, FencingToken: assignment.FencingToken, Authorized: true, ApprovalState: "approved", QueueState: "claimed", ExpiresAt: time.Now().UTC().Add(time.Minute), Kind: kind, TransportKey: assignment.ExecutionInput.RemoteAdapterKey, TargetKey: assignment.TargetKey, RequestText: text}
|
||||
}
|
||||
Reference in New Issue
Block a user