Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 44 additions & 20 deletions internal/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"os"
"strings"
texttemplate "text/template"
"time"

"github.com/arhuman/minexus/internal/config"
Expand All @@ -19,11 +20,12 @@ import (

// WebServer represents the HTTP web server
type WebServer struct {
config *config.NexusConfig
nexus *nexus.Server
templates *template.Template
logger *zap.Logger
startTime time.Time
config *config.NexusConfig
nexus *nexus.Server
templates *template.Template
shellTemplates *texttemplate.Template
logger *zap.Logger
startTime time.Time
}

// NewWebServer creates a new web server instance
Expand All @@ -35,19 +37,20 @@ func NewWebServer(cfg *config.NexusConfig, nexusServer *nexus.Server, logger *za
return nil, fmt.Errorf("failed to load web templates from %s: %w", templatesPath, err)
}

// Load shell script templates
// Load shell script templates using text/template (not html/template)
shellTemplatesPath := fmt.Sprintf("%s/templates/*.sh", cfg.WebRoot)
templates, err = templates.ParseGlob(shellTemplatesPath)
shellTemplates, err := texttemplate.ParseGlob(shellTemplatesPath)
if err != nil {
return nil, fmt.Errorf("failed to load shell script templates from %s: %w", shellTemplatesPath, err)
}

return &WebServer{
config: cfg,
nexus: nexusServer,
templates: templates,
logger: logger,
startTime: time.Now(),
config: cfg,
nexus: nexusServer,
templates: templates,
shellTemplates: shellTemplates,
logger: logger,
startTime: time.Now(),
}, nil
}

Expand Down Expand Up @@ -129,30 +132,51 @@ func (ws *WebServer) handleInstallScript(w http.ResponseWriter, r *http.Request)
return
}

// Build the base URL using NEXUS_SERVER environment variable
// Build the server URL using NEXUS_SERVER environment variable
nexusServer := os.Getenv("NEXUS_SERVER")
if nexusServer == "" {
nexusServer = "localhost"
}
baseURL := fmt.Sprintf("http://%s:%d", nexusServer, ws.config.WebPort)

// Template data
// Template data matching what the script expects
data := struct {
BaseURL string
Date string
ServerURL string
MinionPort int
MinionID string
WebPort int
}{
BaseURL: baseURL,
Date: time.Now().Format("2006-01-02 15:04:05"),
ServerURL: nexusServer,
MinionPort: ws.config.MinionPort,
MinionID: fmt.Sprintf("minion-%d", time.Now().Unix()),
WebPort: ws.config.WebPort,
}

// Set appropriate headers for shell script
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", "inline; filename=install_minion.sh")

// Execute template
if err := ws.templates.ExecuteTemplate(w, "install_minion.sh", data); err != nil {
ws.logger.Error("Failed to execute install script template", zap.Error(err))
// Log diagnostic information
ws.logger.Info("Attempting to execute install script template",
zap.String("template_name", "install_minion.sh"),
zap.String("server_url", nexusServer),
zap.Int("minion_port", ws.config.MinionPort),
zap.Int("web_port", ws.config.WebPort),
zap.String("location", "main"))

// Execute shell template (using text/template, not html/template)
if err := ws.shellTemplates.ExecuteTemplate(w, "install_minion.sh", data); err != nil {
ws.logger.Error("Failed to execute install script template",
zap.String("location", "main"),
zap.Error(err))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

ws.logger.Info("Successfully executed install script template",
zap.String("template_name", "install_minion.sh"),
zap.String("location", "main"))
}

// handleAPIStatus serves the /api/status endpoint
Expand Down
160 changes: 155 additions & 5 deletions internal/web/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"html/template"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
texttemplate "text/template"
"time"

"github.com/arhuman/minexus/internal/config"
Expand All @@ -34,12 +36,54 @@ func createTestWebServer() *WebServer {
templates, _ = GetTemplates()
}

// Create shell script templates
shellTemplatesPath := filepath.Join(cfg.WebRoot, "templates", "*.sh")
shellTemplates, err := texttemplate.ParseGlob(shellTemplatesPath)
if err != nil || shellTemplates == nil || shellTemplates.Lookup("install_minion.sh") == nil {
// Create a minimal test template if webroot doesn't exist or template not found
shellTemplates = texttemplate.New("install_minion.sh")
shellTemplates.Parse(`#!/bin/bash
# Minion Installation Script
# Generated on: {{.Date}}
# Server: {{.ServerURL}}

set -e

# Configuration
NEXUS_SERVER="{{.ServerURL}}"
MINION_PORT="{{.MinionPort}}"
MINION_ID="${MINION_ID:-{{.MinionID}}}"

echo "Installing Minion client..."
echo "Nexus Server: $NEXUS_SERVER"
echo "Minion Port: $MINION_PORT"
echo "Minion ID: $MINION_ID"

# Download and install minion binary
echo "Downloading minion binary..."
curl -o minion "http://$NEXUS_SERVER:{{.WebPort}}/binaries/minion/$(uname -s)-$(uname -m)" || {
echo "Failed to download minion binary"
exit 1
}

chmod +x minion

# Create systemd service or run directly
if [ "$1" = "--systemd" ]; then
echo "Minion installed as systemd service"
else
echo "Starting minion..."
NEXUS_SERVER="$NEXUS_SERVER" NEXUS_MINION_PORT="$MINION_PORT" MINION_ID="$MINION_ID" ./minion
fi`)
}

return &WebServer{
config: cfg,
nexus: nil, // We'll test without a real nexus server
templates: templates,
logger: logger,
startTime: time.Now(),
config: cfg,
nexus: nil, // We'll test without a real nexus server
templates: templates,
shellTemplates: shellTemplates,
logger: logger,
startTime: time.Now(),
}
}

Expand Down Expand Up @@ -624,3 +668,109 @@ func TestWebServerIntegration(t *testing.T) {
t.Errorf("Download index: expected status 200, got %d", resp.StatusCode)
}
}

// TestHandleInstallScript tests that the install script endpoint works correctly
// and uses text/template (not html/template) to avoid parsing errors
func TestHandleInstallScript(t *testing.T) {
webServer := createTestWebServer()

// Test GET request
req := httptest.NewRequest(http.MethodGet, "/install_minion.sh", nil)
w := httptest.NewRecorder()

webServer.handleInstallScript(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}

// Check content type is text/plain (not HTML)
contentType := resp.Header.Get("Content-Type")
if !strings.Contains(contentType, "text/plain") {
t.Errorf("Expected text/plain content type, got %s", contentType)
}

// Check content disposition
contentDisposition := resp.Header.Get("Content-Disposition")
if !strings.Contains(contentDisposition, "install_minion.sh") {
t.Errorf("Expected Content-Disposition to contain 'install_minion.sh', got %s", contentDisposition)
}

body := w.Body.String()

// Verify the script contains expected content
expectedContent := []string{
"#!/bin/bash",
"Minion Installation Script",
"Server:", // Template variable should be replaced
"Minion Port:", // Template variable should be replaced
"systemd service", // This would fail with html/template due to quotes
}

for _, expected := range expectedContent {
if !strings.Contains(body, expected) {
t.Errorf("Expected script to contain '%s', but it was missing", expected)
}
}

// Verify no HTML escaping happened (which would occur with html/template)
// Check that quotes are not escaped
if strings.Contains(body, """) || strings.Contains(body, """) {
t.Error("Found HTML-escaped quotes in shell script, should use text/template not html/template")
}

// Check that angle brackets are not escaped (if any were in the template)
if strings.Contains(body, "<") || strings.Contains(body, ">") {
t.Error("Found HTML-escaped angle brackets in shell script, should use text/template not html/template")
}
}

// TestHandleInstallScriptMethodNotAllowed tests non-GET requests to install script
func TestHandleInstallScriptMethodNotAllowed(t *testing.T) {
webServer := createTestWebServer()

methods := []string{http.MethodPost, http.MethodPut, http.MethodDelete}
for _, method := range methods {
req := httptest.NewRequest(method, "/install_minion.sh", nil)
w := httptest.NewRecorder()

webServer.handleInstallScript(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Errorf("Method %s: expected status 405, got %d", method, resp.StatusCode)
}
}
}

// TestInstallScriptTemplateExecution verifies template variable substitution works
func TestInstallScriptTemplateExecution(t *testing.T) {
webServer := createTestWebServer()

// Set a specific NEXUS_SERVER environment variable for testing
originalNexusServer := os.Getenv("NEXUS_SERVER")
os.Setenv("NEXUS_SERVER", "test.nexus.local")
defer os.Setenv("NEXUS_SERVER", originalNexusServer)

req := httptest.NewRequest(http.MethodGet, "/install_minion.sh", nil)
w := httptest.NewRecorder()

webServer.handleInstallScript(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}

body := w.Body.String()

// Verify template variables were replaced
if !strings.Contains(body, "test.nexus.local") {
t.Error("Expected ServerURL 'test.nexus.local' to be in the output")
}

if !strings.Contains(body, "11972") {
t.Error("Expected MinionPort '11972' to be in the output")
}
}
Loading