Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
597711350a
|
|||
|
7113848de8
|
|||
|
54606c6318
|
@@ -29,7 +29,10 @@
|
||||
"Bash(CGO_ENABLED=0 go build:*)",
|
||||
"Bash(CGO_ENABLED=0 go test:*)",
|
||||
"Bash(app/web/dist/index.html)",
|
||||
"Bash(export CGO_ENABLED=0)"
|
||||
"Bash(export CGO_ENABLED=0)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(CGO_ENABLED=0 ORLY_LOG_LEVEL=debug go test:*)",
|
||||
"Bash(/tmp/test-policy-script.sh)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -83,10 +83,12 @@ type PolicyEvent struct {
|
||||
// It safely serializes the embedded event and additional context fields.
|
||||
func (pe *PolicyEvent) MarshalJSON() ([]byte, error) {
|
||||
if pe.E == nil {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"logged_in_pubkey": pe.LoggedInPubkey,
|
||||
"ip_address": pe.IPAddress,
|
||||
})
|
||||
return json.Marshal(
|
||||
map[string]interface{}{
|
||||
"logged_in_pubkey": pe.LoggedInPubkey,
|
||||
"ip_address": pe.IPAddress,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Create a safe copy of the event for JSON marshaling
|
||||
@@ -227,7 +229,10 @@ func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
|
||||
|
||||
if enabled {
|
||||
if err := policy.LoadFromFile(configPath); err != nil {
|
||||
log.W.F("failed to load policy configuration from %s: %v", configPath, err)
|
||||
log.W.F(
|
||||
"failed to load policy configuration from %s: %v", configPath,
|
||||
err,
|
||||
)
|
||||
log.I.F("using default policy configuration")
|
||||
} else {
|
||||
log.I.F("loaded policy configuration from %s", configPath)
|
||||
@@ -438,7 +443,9 @@ func (sr *ScriptRunner) Start() error {
|
||||
// Monitor the process
|
||||
go sr.monitorProcess()
|
||||
|
||||
log.I.F("policy script started: %s (pid=%d)", sr.scriptPath, cmd.Process.Pid)
|
||||
log.I.F(
|
||||
"policy script started: %s (pid=%d)", sr.scriptPath, cmd.Process.Pid,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -473,7 +480,10 @@ func (sr *ScriptRunner) Stop() error {
|
||||
log.I.F("policy script stopped: %s", sr.scriptPath)
|
||||
case <-time.After(5 * time.Second):
|
||||
// Force kill after 5 seconds
|
||||
log.W.F("policy script did not stop gracefully, sending SIGKILL: %s", sr.scriptPath)
|
||||
log.W.F(
|
||||
"policy script did not stop gracefully, sending SIGKILL: %s",
|
||||
sr.scriptPath,
|
||||
)
|
||||
if err := sr.currentCmd.Process.Kill(); chk.E(err) {
|
||||
log.E.F("failed to kill script process: %v", err)
|
||||
}
|
||||
@@ -502,7 +512,10 @@ func (sr *ScriptRunner) Stop() error {
|
||||
}
|
||||
|
||||
// ProcessEvent sends an event to the script and waits for a response.
|
||||
func (sr *ScriptRunner) ProcessEvent(evt *PolicyEvent) (*PolicyResponse, error) {
|
||||
func (sr *ScriptRunner) ProcessEvent(evt *PolicyEvent) (
|
||||
*PolicyResponse, error,
|
||||
) {
|
||||
log.D.F("processing event: %s", evt.Serialize())
|
||||
sr.mutex.RLock()
|
||||
if !sr.isRunning || sr.stdin == nil {
|
||||
sr.mutex.RUnlock()
|
||||
@@ -525,6 +538,7 @@ func (sr *ScriptRunner) ProcessEvent(evt *PolicyEvent) (*PolicyResponse, error)
|
||||
// Wait for response with timeout
|
||||
select {
|
||||
case response := <-sr.responseChan:
|
||||
log.D.S("response", response)
|
||||
return &response, nil
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("script response timeout")
|
||||
@@ -545,10 +559,13 @@ func (sr *ScriptRunner) readResponses() {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
log.D.F("policy response: %s", line)
|
||||
var response PolicyResponse
|
||||
if err := json.Unmarshal([]byte(line), &response); chk.E(err) {
|
||||
log.E.F("failed to parse policy response from %s: %v", sr.scriptPath, err)
|
||||
log.E.F(
|
||||
"failed to parse policy response from %s: %v", sr.scriptPath,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -556,12 +573,17 @@ func (sr *ScriptRunner) readResponses() {
|
||||
select {
|
||||
case sr.responseChan <- response:
|
||||
default:
|
||||
log.W.F("policy response channel full for %s, dropping response", sr.scriptPath)
|
||||
log.W.F(
|
||||
"policy response channel full for %s, dropping response",
|
||||
sr.scriptPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); chk.E(err) {
|
||||
log.E.F("error reading policy responses from %s: %v", sr.scriptPath, err)
|
||||
log.E.F(
|
||||
"error reading policy responses from %s: %v", sr.scriptPath, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +627,10 @@ func (sr *ScriptRunner) monitorProcess() {
|
||||
sr.currentCancel = nil
|
||||
|
||||
if err != nil {
|
||||
log.E.F("policy script exited with error: %s: %v, will retry periodically", sr.scriptPath, err)
|
||||
log.E.F(
|
||||
"policy script exited with error: %s: %v, will retry periodically",
|
||||
sr.scriptPath, err,
|
||||
)
|
||||
} else {
|
||||
log.I.F("policy script exited normally: %s", sr.scriptPath)
|
||||
}
|
||||
@@ -631,9 +656,15 @@ func (sr *ScriptRunner) periodicCheck() {
|
||||
// Script exists but not running, try to start
|
||||
go func() {
|
||||
if err := sr.Start(); err != nil {
|
||||
log.E.F("failed to restart policy script %s: %v, will retry in next cycle", sr.scriptPath, err)
|
||||
log.E.F(
|
||||
"failed to restart policy script %s: %v, will retry in next cycle",
|
||||
sr.scriptPath, err,
|
||||
)
|
||||
} else {
|
||||
log.I.F("policy script restarted successfully: %s", sr.scriptPath)
|
||||
log.I.F(
|
||||
"policy script restarted successfully: %s",
|
||||
sr.scriptPath,
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -646,7 +677,9 @@ func (sr *ScriptRunner) periodicCheck() {
|
||||
// Returns an error if the file doesn't exist, can't be read, or contains invalid JSON.
|
||||
func (p *P) LoadFromFile(configPath string) error {
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("policy configuration file does not exist: %s", configPath)
|
||||
return fmt.Errorf(
|
||||
"policy configuration file does not exist: %s", configPath,
|
||||
)
|
||||
}
|
||||
|
||||
configData, err := os.ReadFile(configPath)
|
||||
@@ -669,7 +702,9 @@ func (p *P) LoadFromFile(configPath string) error {
|
||||
// The access parameter should be "write" for accepting events or "read" for filtering events.
|
||||
// Returns true if the event is allowed, false if denied, and an error if validation fails.
|
||||
// Policy evaluation order: global rules → kind filtering → specific rules → default policy.
|
||||
func (p *P) CheckPolicy(access string, ev *event.E, loggedInPubkey []byte, ipAddress string) (allowed bool, err error) {
|
||||
func (p *P) CheckPolicy(
|
||||
access string, ev *event.E, loggedInPubkey []byte, ipAddress string,
|
||||
) (allowed bool, err error) {
|
||||
// Handle nil event
|
||||
if ev == nil {
|
||||
return false, fmt.Errorf("event cannot be nil")
|
||||
@@ -698,18 +733,35 @@ func (p *P) CheckPolicy(access string, ev *event.E, loggedInPubkey []byte, ipAdd
|
||||
// Check if script file exists before trying to use it
|
||||
if _, err := os.Stat(rule.Script); err == nil {
|
||||
// Script exists, try to use it
|
||||
allowed, err := p.checkScriptPolicy(access, ev, rule.Script, loggedInPubkey, ipAddress)
|
||||
log.D.F(
|
||||
"using policy script for kind %d: %s", ev.Kind, rule.Script,
|
||||
)
|
||||
allowed, err := p.checkScriptPolicy(
|
||||
access, ev, rule.Script, loggedInPubkey, ipAddress,
|
||||
)
|
||||
if err == nil {
|
||||
// Script ran successfully, return its decision
|
||||
return allowed, nil
|
||||
}
|
||||
// Script failed, fall through to apply other criteria
|
||||
log.W.F("policy script check failed for kind %d: %v, applying other criteria", ev.Kind, err)
|
||||
log.W.F(
|
||||
"policy script check failed for kind %d: %v, applying other criteria",
|
||||
ev.Kind, err,
|
||||
)
|
||||
} else {
|
||||
// Script configured but doesn't exist
|
||||
log.W.F(
|
||||
"policy script configured for kind %d but not found at %s: %v, applying other criteria",
|
||||
ev.Kind, rule.Script, err,
|
||||
)
|
||||
}
|
||||
// Script doesn't exist or failed, fall through to apply other criteria
|
||||
} else {
|
||||
// Policy manager is disabled, fall back to default policy
|
||||
log.D.F("policy manager is disabled for kind %d, falling back to default policy (%s)", ev.Kind, p.DefaultPolicy)
|
||||
log.D.F(
|
||||
"policy manager is disabled for kind %d, falling back to default policy (%s)",
|
||||
ev.Kind, p.DefaultPolicy,
|
||||
)
|
||||
return p.getDefaultPolicyAction(), nil
|
||||
}
|
||||
}
|
||||
@@ -743,7 +795,9 @@ func (p *P) checkKindsPolicy(kind uint16) bool {
|
||||
}
|
||||
|
||||
// checkGlobalRulePolicy checks if the event passes the global rule filter
|
||||
func (p *P) checkGlobalRulePolicy(access string, ev *event.E, loggedInPubkey []byte) bool {
|
||||
func (p *P) checkGlobalRulePolicy(
|
||||
access string, ev *event.E, loggedInPubkey []byte,
|
||||
) bool {
|
||||
// Apply global rule filtering
|
||||
allowed, err := p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
|
||||
if err != nil {
|
||||
@@ -754,7 +808,9 @@ func (p *P) checkGlobalRulePolicy(access string, ev *event.E, loggedInPubkey []b
|
||||
}
|
||||
|
||||
// checkRulePolicy applies rule-based filtering (pubkey lists, size limits, etc.)
|
||||
func (p *P) checkRulePolicy(access string, ev *event.E, rule Rule, loggedInPubkey []byte) (allowed bool, err error) {
|
||||
func (p *P) checkRulePolicy(
|
||||
access string, ev *event.E, rule Rule, loggedInPubkey []byte,
|
||||
) (allowed bool, err error) {
|
||||
pubkeyHex := hex.Enc(ev.Pubkey)
|
||||
|
||||
// Check pubkey-based access control
|
||||
@@ -882,21 +938,29 @@ func (p *P) checkRulePolicy(access string, ev *event.E, rule Rule, loggedInPubke
|
||||
}
|
||||
|
||||
// checkScriptPolicy runs the policy script to determine if event should be allowed
|
||||
func (p *P) checkScriptPolicy(access string, ev *event.E, scriptPath string, loggedInPubkey []byte, ipAddress string) (allowed bool, err error) {
|
||||
func (p *P) checkScriptPolicy(
|
||||
access string, ev *event.E, scriptPath string, loggedInPubkey []byte,
|
||||
ipAddress string,
|
||||
) (allowed bool, err error) {
|
||||
if p.Manager == nil {
|
||||
return false, fmt.Errorf("policy manager is not initialized")
|
||||
}
|
||||
|
||||
// If policy is disabled, fall back to default policy immediately
|
||||
if !p.Manager.IsEnabled() {
|
||||
log.W.F("policy rule for kind %d is inactive (policy disabled), falling back to default policy (%s)", ev.Kind, p.DefaultPolicy)
|
||||
log.W.F(
|
||||
"policy rule for kind %d is inactive (policy disabled), falling back to default policy (%s)",
|
||||
ev.Kind, p.DefaultPolicy,
|
||||
)
|
||||
return p.getDefaultPolicyAction(), nil
|
||||
}
|
||||
|
||||
// Check if script file exists
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
// Script doesn't exist, return error so caller can fall back to other criteria
|
||||
return false, fmt.Errorf("policy script does not exist at %s", scriptPath)
|
||||
return false, fmt.Errorf(
|
||||
"policy script does not exist at %s", scriptPath,
|
||||
)
|
||||
}
|
||||
|
||||
// Get or create a runner for this specific script path
|
||||
@@ -905,10 +969,14 @@ func (p *P) checkScriptPolicy(access string, ev *event.E, scriptPath string, log
|
||||
// Policy is enabled, check if this runner is running
|
||||
if !runner.IsRunning() {
|
||||
// Try to start this runner and wait for it
|
||||
log.D.F("starting policy script for kind %d: %s", ev.Kind, scriptPath)
|
||||
if err := runner.ensureRunning(); err != nil {
|
||||
// Startup failed, return error so caller can fall back to other criteria
|
||||
return false, fmt.Errorf("failed to start policy script: %v", err)
|
||||
return false, fmt.Errorf(
|
||||
"failed to start policy script %s: %v", scriptPath, err,
|
||||
)
|
||||
}
|
||||
log.I.F("policy script started for kind %d: %s", ev.Kind, scriptPath)
|
||||
}
|
||||
|
||||
// Create policy event with additional context
|
||||
@@ -921,7 +989,10 @@ func (p *P) checkScriptPolicy(access string, ev *event.E, scriptPath string, log
|
||||
// Process event through policy script
|
||||
response, scriptErr := runner.ProcessEvent(policyEvent)
|
||||
if chk.E(scriptErr) {
|
||||
log.E.F("policy rule for kind %d failed (script processing error: %v), falling back to default policy (%s)", ev.Kind, scriptErr, p.DefaultPolicy)
|
||||
log.E.F(
|
||||
"policy rule for kind %d failed (script processing error: %v), falling back to default policy (%s)",
|
||||
ev.Kind, scriptErr, p.DefaultPolicy,
|
||||
)
|
||||
// Fall back to default policy on script failure
|
||||
return p.getDefaultPolicyAction(), nil
|
||||
}
|
||||
@@ -935,7 +1006,10 @@ func (p *P) checkScriptPolicy(access string, ev *event.E, scriptPath string, log
|
||||
case "shadowReject":
|
||||
return false, nil // Treat as reject for policy purposes
|
||||
default:
|
||||
log.W.F("policy rule for kind %d returned unknown action '%s', falling back to default policy (%s)", ev.Kind, response.Action, p.DefaultPolicy)
|
||||
log.W.F(
|
||||
"policy rule for kind %d returned unknown action '%s', falling back to default policy (%s)",
|
||||
ev.Kind, response.Action, p.DefaultPolicy,
|
||||
)
|
||||
// Fall back to default policy for unknown actions
|
||||
return p.getDefaultPolicyAction(), nil
|
||||
}
|
||||
@@ -953,16 +1027,21 @@ func (pm *PolicyManager) periodicCheck() {
|
||||
|
||||
// startPolicyIfExists starts the default policy script if the file exists.
|
||||
// This is for backward compatibility with the default script path.
|
||||
// Only logs if the default script actually exists - missing default scripts are normal
|
||||
// when users configure rule-specific scripts.
|
||||
func (pm *PolicyManager) startPolicyIfExists() {
|
||||
if _, err := os.Stat(pm.scriptPath); err == nil {
|
||||
// Get or create runner for the default script, which will start it
|
||||
// Default script exists, try to start it
|
||||
log.I.F("found default policy script at %s, starting...", pm.scriptPath)
|
||||
runner := pm.getOrCreateRunner(pm.scriptPath)
|
||||
if err := runner.Start(); err != nil {
|
||||
log.E.F("failed to start default policy script: %v, will retry periodically", err)
|
||||
log.E.F(
|
||||
"failed to start default policy script: %v, will retry periodically",
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
log.W.F("default policy script not found at %s, will be started if it appears", pm.scriptPath)
|
||||
}
|
||||
// Silently ignore if default script doesn't exist - it's fine if rules use custom scripts
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the policy manager is enabled.
|
||||
|
||||
@@ -1514,6 +1514,213 @@ func TestDefaultPolicyLogicWithRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleScriptLoading(t *testing.T) {
|
||||
// This test validates that a policy script loads for a specific Rule
|
||||
// and properly processes events
|
||||
|
||||
// Create temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tempDir, "test-rule-script.sh")
|
||||
|
||||
// Create a test script that accepts events with "allowed" in content
|
||||
scriptContent := `#!/bin/bash
|
||||
while IFS= read -r line; do
|
||||
if echo "$line" | grep -q 'allowed'; then
|
||||
echo '{"action":"accept","msg":"Content approved"}'
|
||||
else
|
||||
echo '{"action":"reject","msg":"Content not allowed"}'
|
||||
fi
|
||||
done
|
||||
`
|
||||
err := os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test script: %v", err)
|
||||
}
|
||||
|
||||
// Create policy manager with script support
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
manager := &PolicyManager{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
configDir: tempDir,
|
||||
scriptPath: filepath.Join(tempDir, "default-policy.sh"), // Different from rule script
|
||||
enabled: true,
|
||||
runners: make(map[string]*ScriptRunner),
|
||||
}
|
||||
|
||||
// Create policy with a rule that uses the script
|
||||
policy := &P{
|
||||
DefaultPolicy: "deny",
|
||||
Manager: manager,
|
||||
Rules: map[int]Rule{
|
||||
4678: {
|
||||
Description: "Test rule with custom script",
|
||||
Script: scriptPath, // Rule-specific script path
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Generate test keypairs
|
||||
eventSigner, eventPubkey := generateTestKeypair(t)
|
||||
|
||||
// Pre-start the script before running tests
|
||||
runner := manager.getOrCreateRunner(scriptPath)
|
||||
err = runner.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start script: %v", err)
|
||||
}
|
||||
|
||||
// Wait for script to be ready
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if !runner.IsRunning() {
|
||||
t.Fatal("Script should be running after Start()")
|
||||
}
|
||||
|
||||
// Test sending a warmup event to ensure script is responsive
|
||||
signer := p8k.MustNew()
|
||||
signer.Generate()
|
||||
warmupEv := event.New()
|
||||
warmupEv.CreatedAt = time.Now().Unix()
|
||||
warmupEv.Kind = 4678
|
||||
warmupEv.Content = []byte("warmup")
|
||||
warmupEv.Tags = tag.NewS()
|
||||
warmupEv.Sign(signer)
|
||||
|
||||
warmupEvent := &PolicyEvent{
|
||||
E: warmupEv,
|
||||
IPAddress: "127.0.0.1",
|
||||
}
|
||||
|
||||
// Send warmup event to verify script is responding
|
||||
_, err = runner.ProcessEvent(warmupEvent)
|
||||
if err != nil {
|
||||
t.Fatalf("Script not responding to warmup event: %v", err)
|
||||
}
|
||||
|
||||
t.Log("Script is ready and responding")
|
||||
|
||||
// Test 1: Event with "allowed" content should be accepted
|
||||
t.Run("script_accepts_allowed_content", func(t *testing.T) {
|
||||
testEvent := createTestEvent(t, eventSigner, "this is allowed content", 4678)
|
||||
|
||||
allowed, err := policy.CheckPolicy("write", testEvent, eventPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Logf("Policy check failed: %v", err)
|
||||
// Check if script exists
|
||||
if _, statErr := os.Stat(scriptPath); statErr != nil {
|
||||
t.Errorf("Script file error: %v", statErr)
|
||||
}
|
||||
t.Fatalf("Unexpected error during policy check: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected event with 'allowed' content to be accepted by script")
|
||||
t.Logf("Event content: %s", string(testEvent.Content))
|
||||
}
|
||||
|
||||
// Verify the script runner was created and is running
|
||||
manager.mutex.RLock()
|
||||
runner, exists := manager.runners[scriptPath]
|
||||
manager.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatal("Expected script runner to be created for rule script path")
|
||||
}
|
||||
if !runner.IsRunning() {
|
||||
t.Error("Expected script runner to be running after processing event")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 2: Event without "allowed" content should be rejected
|
||||
t.Run("script_rejects_disallowed_content", func(t *testing.T) {
|
||||
testEvent := createTestEvent(t, eventSigner, "this is not permitted", 4678)
|
||||
|
||||
allowed, err := policy.CheckPolicy("write", testEvent, eventPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected event without 'allowed' content to be rejected by script")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Verify script path is correct (rule-specific, not default)
|
||||
t.Run("script_path_is_rule_specific", func(t *testing.T) {
|
||||
manager.mutex.RLock()
|
||||
runner, exists := manager.runners[scriptPath]
|
||||
_, defaultExists := manager.runners[manager.scriptPath]
|
||||
manager.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatal("Expected rule-specific script runner to exist")
|
||||
}
|
||||
if defaultExists {
|
||||
t.Error("Default script runner should not be created when only rule-specific scripts are used")
|
||||
}
|
||||
|
||||
// Verify the runner is using the correct script path
|
||||
if runner.scriptPath != scriptPath {
|
||||
t.Errorf("Expected runner to use script path %s, got %s", scriptPath, runner.scriptPath)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Multiple events should use the same script instance
|
||||
t.Run("script_reused_for_multiple_events", func(t *testing.T) {
|
||||
// Get initial runner
|
||||
manager.mutex.RLock()
|
||||
initialRunner, _ := manager.runners[scriptPath]
|
||||
initialRunnerCount := len(manager.runners)
|
||||
manager.mutex.RUnlock()
|
||||
|
||||
// Process multiple events
|
||||
for i := 0; i < 5; i++ {
|
||||
content := "this is allowed message " + string(rune('0'+i))
|
||||
testEvent := createTestEvent(t, eventSigner, content, 4678)
|
||||
_, err := policy.CheckPolicy("write", testEvent, eventPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error on event %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify same runner is used
|
||||
manager.mutex.RLock()
|
||||
currentRunner, _ := manager.runners[scriptPath]
|
||||
currentRunnerCount := len(manager.runners)
|
||||
manager.mutex.RUnlock()
|
||||
|
||||
if currentRunner != initialRunner {
|
||||
t.Error("Expected same runner instance to be reused for multiple events")
|
||||
}
|
||||
if currentRunnerCount != initialRunnerCount {
|
||||
t.Errorf("Expected runner count to stay at %d, got %d", initialRunnerCount, currentRunnerCount)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 5: Different kind without script should use default policy
|
||||
t.Run("different_kind_uses_default_policy", func(t *testing.T) {
|
||||
testEvent := createTestEvent(t, eventSigner, "any content", 1) // Kind 1 has no rule
|
||||
|
||||
allowed, err := policy.CheckPolicy("write", testEvent, eventPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
// Should be denied by default policy (deny)
|
||||
if allowed {
|
||||
t.Error("Expected event of kind without rule to be denied by default policy")
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup: Stop the script
|
||||
manager.mutex.RLock()
|
||||
runner, exists := manager.runners[scriptPath]
|
||||
manager.mutex.RUnlock()
|
||||
if exists && runner.IsRunning() {
|
||||
runner.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyFilterProcessing(t *testing.T) {
|
||||
// Test policy filter processing using the provided filter JSON specification
|
||||
filterJSON := []byte(`{
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.27.1
|
||||
v0.27.3
|
||||
154
scripts/BOOTSTRAP.md
Normal file
154
scripts/BOOTSTRAP.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# ORLY Relay Bootstrap Script
|
||||
|
||||
This directory contains a bootstrap script that automates the deployment of the ORLY relay.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### One-Line Installation
|
||||
|
||||
Clone the repository and deploy the relay with a single command:
|
||||
|
||||
```bash
|
||||
curl -sSL https://git.nostrdev.com/mleku/next.orly.dev/raw/branch/main/scripts/bootstrap.sh | bash
|
||||
```
|
||||
|
||||
**Note:** This assumes the script is accessible at the raw URL path. Adjust the URL based on your git server's raw file URL format.
|
||||
|
||||
### Alternative: Download and Execute
|
||||
|
||||
If you prefer to review the script before running it:
|
||||
|
||||
```bash
|
||||
# Download the script
|
||||
curl -o bootstrap.sh https://git.nostrdev.com/mleku/next.orly.dev/raw/branch/main/scripts/bootstrap.sh
|
||||
|
||||
# Review the script
|
||||
cat bootstrap.sh
|
||||
|
||||
# Make it executable and run
|
||||
chmod +x bootstrap.sh
|
||||
./bootstrap.sh
|
||||
```
|
||||
|
||||
## What the Bootstrap Script Does
|
||||
|
||||
1. **Checks Prerequisites**
|
||||
- Verifies that `git` is installed on your system
|
||||
|
||||
2. **Clones or Updates Repository**
|
||||
- Clones the repository to `~/src/next.orly.dev` if it doesn't exist
|
||||
- If the repository already exists, pulls the latest changes from the main branch
|
||||
- Stashes any local changes before updating
|
||||
|
||||
3. **Runs Deployment**
|
||||
- Executes `scripts/deploy.sh` to:
|
||||
- Install Go if needed
|
||||
- Build the ORLY relay with embedded web UI
|
||||
- Install the binary to `~/.local/bin/orly`
|
||||
- Set up systemd service
|
||||
- Configure necessary capabilities
|
||||
|
||||
4. **Provides Next Steps**
|
||||
- Shows commands to start, check status, and view logs
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After the bootstrap script completes, you can:
|
||||
|
||||
### Start the relay
|
||||
```bash
|
||||
sudo systemctl start orly
|
||||
```
|
||||
|
||||
### Enable on boot
|
||||
```bash
|
||||
sudo systemctl enable orly
|
||||
```
|
||||
|
||||
### Check status
|
||||
```bash
|
||||
sudo systemctl status orly
|
||||
```
|
||||
|
||||
### View logs
|
||||
```bash
|
||||
sudo journalctl -u orly -f
|
||||
```
|
||||
|
||||
### View relay identity
|
||||
```bash
|
||||
~/.local/bin/orly identity
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The relay configuration is managed through environment variables. Edit the systemd service file to configure:
|
||||
|
||||
```bash
|
||||
sudo systemctl edit orly
|
||||
```
|
||||
|
||||
See the main README.md for available configuration options.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Git Not Found
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get update && sudo apt-get install -y git
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install -y git
|
||||
|
||||
# Arch
|
||||
sudo pacman -S git
|
||||
```
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
Make sure your user has sudo privileges for systemd service management.
|
||||
|
||||
### Port 443 Already in Use
|
||||
|
||||
If you're running TLS on port 443, make sure no other service is using that port:
|
||||
|
||||
```bash
|
||||
sudo netstat -tlnp | grep :443
|
||||
```
|
||||
|
||||
### Script Fails to Clone
|
||||
|
||||
If the repository URL is not accessible, you may need to:
|
||||
- Check your network connection
|
||||
- Verify the git server is accessible
|
||||
- Use SSH URL instead (modify the script's `REPO_URL` variable)
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
If you prefer to deploy manually without the bootstrap script:
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.nostrdev.com/mleku/next.orly.dev.git ~/src/next.orly.dev
|
||||
|
||||
# Enter directory
|
||||
cd ~/src/next.orly.dev
|
||||
|
||||
# Run deployment
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When running scripts from the internet:
|
||||
1. Always review the script contents before execution
|
||||
2. Use HTTPS URLs to prevent man-in-the-middle attacks
|
||||
3. Verify the source is trustworthy
|
||||
4. Consider using the "download and review" method instead of piping directly to bash
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Open an issue on the git repository
|
||||
- Check the main README.md for detailed documentation
|
||||
- Review logs with `sudo journalctl -u orly -f`
|
||||
138
scripts/bootstrap.sh
Executable file
138
scripts/bootstrap.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Bootstrap script for ORLY relay
|
||||
#
|
||||
# This script clones the ORLY repository and runs the deployment script.
|
||||
# It can be executed directly via curl:
|
||||
#
|
||||
# curl -sSL https://git.nostrdev.com/mleku/next.orly.dev/raw/branch/main/scripts/bootstrap.sh | bash
|
||||
#
|
||||
# Or downloaded and executed:
|
||||
#
|
||||
# curl -o bootstrap.sh https://git.nostrdev.com/mleku/next.orly.dev/raw/branch/main/scripts/bootstrap.sh
|
||||
# chmod +x bootstrap.sh
|
||||
# ./bootstrap.sh
|
||||
|
||||
set -e # Exit on error
|
||||
set -u # Exit on undefined variable
|
||||
set -o pipefail # Exit on pipe failure
|
||||
|
||||
# Configuration
|
||||
REPO_URL="https://git.nostrdev.com/mleku/next.orly.dev.git"
|
||||
REPO_NAME="next.orly.dev"
|
||||
CLONE_DIR="${HOME}/src/${REPO_NAME}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print functions
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Error handler
|
||||
error_exit() {
|
||||
print_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if git is installed
|
||||
check_git() {
|
||||
if ! command -v git &> /dev/null; then
|
||||
error_exit "git is not installed. Please install git and try again."
|
||||
fi
|
||||
print_success "git is installed"
|
||||
}
|
||||
|
||||
# Clone or update repository
|
||||
clone_or_update_repo() {
|
||||
if [ -d "${CLONE_DIR}/.git" ]; then
|
||||
print_info "Repository already exists at ${CLONE_DIR}"
|
||||
print_info "Updating repository..."
|
||||
|
||||
cd "${CLONE_DIR}" || error_exit "Failed to change to directory ${CLONE_DIR}"
|
||||
|
||||
# Stash any local changes
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
print_warning "Local changes detected. Stashing them..."
|
||||
git stash || error_exit "Failed to stash changes"
|
||||
fi
|
||||
|
||||
# Pull latest changes
|
||||
git pull origin main || error_exit "Failed to update repository"
|
||||
print_success "Repository updated successfully"
|
||||
else
|
||||
print_info "Cloning repository from ${REPO_URL}..."
|
||||
|
||||
# Create parent directory if it doesn't exist
|
||||
mkdir -p "$(dirname "${CLONE_DIR}")" || error_exit "Failed to create directory $(dirname "${CLONE_DIR}")"
|
||||
|
||||
# Clone the repository
|
||||
git clone "${REPO_URL}" "${CLONE_DIR}" || error_exit "Failed to clone repository"
|
||||
print_success "Repository cloned successfully to ${CLONE_DIR}"
|
||||
|
||||
cd "${CLONE_DIR}" || error_exit "Failed to change to directory ${CLONE_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run deployment script
|
||||
run_deployment() {
|
||||
print_info "Running deployment script..."
|
||||
|
||||
if [ ! -f "${CLONE_DIR}/scripts/deploy.sh" ]; then
|
||||
error_exit "Deployment script not found at ${CLONE_DIR}/scripts/deploy.sh"
|
||||
fi
|
||||
|
||||
chmod +x "${CLONE_DIR}/scripts/deploy.sh" || error_exit "Failed to make deployment script executable"
|
||||
|
||||
"${CLONE_DIR}/scripts/deploy.sh" || error_exit "Deployment failed"
|
||||
|
||||
print_success "Deployment completed successfully!"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo ""
|
||||
print_info "ORLY Relay Bootstrap Script"
|
||||
print_info "=============================="
|
||||
echo ""
|
||||
|
||||
check_git
|
||||
clone_or_update_repo
|
||||
run_deployment
|
||||
|
||||
echo ""
|
||||
print_success "Bootstrap process completed successfully!"
|
||||
echo ""
|
||||
print_info "The ORLY relay has been deployed."
|
||||
print_info "Repository location: ${CLONE_DIR}"
|
||||
echo ""
|
||||
print_info "To start the relay service:"
|
||||
echo " sudo systemctl start orly"
|
||||
echo ""
|
||||
print_info "To check the relay status:"
|
||||
echo " sudo systemctl status orly"
|
||||
echo ""
|
||||
print_info "To view relay logs:"
|
||||
echo " sudo journalctl -u orly -f"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
Reference in New Issue
Block a user