added custom web html

This commit is contained in:
2025-09-28 21:28:39 +01:00
parent 22185904be
commit fde81e982a
17 changed files with 2403 additions and 53 deletions

BIN
app.db

Binary file not shown.

View File

@@ -7,15 +7,32 @@ import (
"path/filepath"
)
// WebServiceConfig defines configuration for a custom web honeypot service
type WebServiceConfig struct {
Enabled bool `json:"enabled"`
Port int `json:"port"`
Name string `json:"name"` // Service name (e.g., "admin-panel", "webmail")
Path string `json:"path"` // URL path (e.g., "/login", "/admin")
TemplateName string `json:"template_name"` // Template filename (e.g., "admin-login.html")
UseHTTPS bool `json:"use_https"` // Whether to use HTTPS
}
// WebServicesConfig holds configuration for custom web services
type WebServicesConfig struct {
Generic []WebServiceConfig `json:"generic"`
}
// Config contains runtime configuration for the honeypot
type Config struct {
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
LogPath string `json:"log_path"`
Web struct {
Enabled bool `json:"enabled"`
Bind string `json:"bind"`
Port int `json:"port"`
Enabled bool `json:"enabled"`
Bind string `json:"bind"`
Port int `json:"port"`
HTTPTemplateName string `json:"http_template_name"` // Optional template for HTTP service
HTTPSTemplateName string `json:"https_template_name"` // Optional template for HTTPS service
} `json:"web"`
Services struct {
@@ -33,9 +50,10 @@ type Config struct {
SMB bool `json:"smb"`
SIP bool `json:"sip"`
VNC bool `json:"vnc"`
Generic []int `json:"generic"`
} `json:"services"`
WebServices WebServicesConfig `json:"web_services"`
Ports struct {
HTTP int `json:"http"`
HTTPS int `json:"https"`
@@ -145,7 +163,6 @@ func defaultConfig() Config {
c.Services.SMB = false
c.Services.SIP = false
c.Services.VNC = false
c.Services.Generic = []int{}
// Standard ports
c.Ports.HTTP = 8080
@@ -174,5 +191,17 @@ func defaultConfig() Config {
c.Security.BlockHighThreatIPs = false
c.Security.ThreatScoreThreshold = 80
// Web services defaults - only generic services, configurable from dashboard
c.WebServices.Generic = []WebServiceConfig{
{
Enabled: false,
Port: 9001,
Name: "admin-panel",
Path: "/admin",
TemplateName: "admin-login.html",
UseHTTPS: false,
},
}
return c
}

View File

@@ -0,0 +1,95 @@
package dashboard
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// ConfigManager manages web services configuration
type ConfigManager struct {
configPath string
}
// NewConfigManager creates a new config manager
func NewConfigManager(configPath string) *ConfigManager {
return &ConfigManager{
configPath: configPath,
}
}
// GetWebServices returns the current web services configuration
func (cm *ConfigManager) GetWebServices() []WebServiceConfig {
// Try to read from config file
if data, err := os.ReadFile(cm.configPath); err == nil {
var config struct {
WebServices struct {
Generic []WebServiceConfig `json:"generic"`
} `json:"web_services"`
}
if err := json.Unmarshal(data, &config); err == nil {
// If web services section exists, return it (even if empty)
return config.WebServices.Generic
}
}
// Only return default configuration on first run (when config doesn't exist)
// Check if this is truly the first run by looking for any config file
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
// First run - create default configuration
defaultServices := []WebServiceConfig{
{
Enabled: false,
Port: 9001,
Name: "admin-panel",
Path: "/admin",
TemplateName: "admin-login.html",
UseHTTPS: false,
},
}
// Save the default configuration so it can be modified later
cm.SaveWebServices(defaultServices)
return defaultServices
}
// Config file exists but web_services section is missing or corrupted
// Return empty slice to allow full user control
return []WebServiceConfig{}
}
// SaveWebServices saves the web services configuration
func (cm *ConfigManager) SaveWebServices(services []WebServiceConfig) error {
// Read existing config
var config map[string]interface{}
if data, err := os.ReadFile(cm.configPath); err == nil {
json.Unmarshal(data, &config)
} else {
config = make(map[string]interface{})
}
// Update web services section
if config["web_services"] == nil {
config["web_services"] = make(map[string]interface{})
}
webServices := config["web_services"].(map[string]interface{})
webServices["generic"] = services
// Write back to file
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(cm.configPath), 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}

View File

@@ -1,6 +1,7 @@
package dashboard
import (
"context"
"encoding/json"
"log"
"strings"
@@ -15,6 +16,8 @@ type ThreatManager struct {
securityManager *SecurityManager
userAPI *UserAPI
blocklistExporter *BlocklistExporter
webTemplateAPI *WebTemplateAPI
webServicesAPI *WebServicesAPI
}
// NewThreatManager creates a new threat manager instance
@@ -29,23 +32,36 @@ func NewThreatManager(dbPath string) (*ThreatManager, error) {
if err != nil {
return nil, err
}
// Initialize security manager
securityManager := NewSecurityManager(authManager)
// Initialize APIs
api := NewThreatAPI(analyzer)
userAPI := NewUserAPI(authManager, securityManager)
// Initialize blocklist exporter
blocklistExporter := NewBlocklistExporter(analyzer)
return &ThreatManager{
analyzer: analyzer,
api: api,
authManager: authManager,
securityManager: securityManager,
userAPI: userAPI,
// Initialize web template API
templateManager := NewWebTemplateManager("webtemplates")
webTemplateAPI := NewWebTemplateAPI(templateManager)
// Initialize web services API
configManager := NewConfigManager("config.json")
webServicesAPI := NewWebServicesAPI(configManager)
tm := &ThreatManager{
analyzer: analyzer,
api: api,
authManager: authManager,
securityManager: securityManager,
userAPI: userAPI,
blocklistExporter: blocklistExporter,
}, nil
webTemplateAPI: webTemplateAPI,
webServicesAPI: webServicesAPI,
}
return tm, nil
}
// ProcessHoneypotRecord processes a honeypot log record for threat analysis
@@ -151,22 +167,30 @@ func (tm *ThreatManager) GetAPI() *ThreatAPI {
func (tm *ThreatManager) GetSecurityManager() *SecurityManager {
return tm.securityManager
}
// GetUserAPI returns the user API instance
func (tm *ThreatManager) GetUserAPI() *UserAPI {
return tm.userAPI
}
// GetBlocklistExporter returns the blocklist exporter instance
// GetBlocklistExporter returns the blocklist exporter
func (tm *ThreatManager) GetBlocklistExporter() *BlocklistExporter {
return tm.blocklistExporter
}
// GetWebTemplateAPI returns the web template API
func (tm *ThreatManager) GetWebTemplateAPI() *WebTemplateAPI {
return tm.webTemplateAPI
}
// GetWebServicesAPI returns the web services API
func (tm *ThreatManager) GetWebServicesAPI() *WebServicesAPI {
return tm.webServicesAPI
}
// GetBlockedIPs returns currently blocked IPs for firewall integration
func (tm *ThreatManager) GetBlockedIPs() ([]string, error) {
return tm.analyzer.GetBlockedIPs()
}
// IsIPBlocked checks if an IP is currently blocked
func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
report, err := tm.analyzer.AnalyzeIP(ip)
@@ -177,15 +201,23 @@ func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
}
// RunPeriodicTasks starts background tasks for threat analysis
func (tm *ThreatManager) RunPeriodicTasks() {
// Run threat score calculation every 5 minutes
func (tm *ThreatManager) RunPeriodicTasks(ctx context.Context) {
// Update threat scores every 5 minutes
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := tm.updateThreatScores(); err != nil {
log.Printf("Failed to update threat scores: %v", err)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := tm.updateThreatScores(); err != nil {
// Only log if not a context cancellation or database closed error
if ctx.Err() == nil && !strings.Contains(err.Error(), "database is closed") {
log.Printf("Failed to update threat scores: %v", err)
}
}
}
}
}()
@@ -195,9 +227,17 @@ func (tm *ThreatManager) RunPeriodicTasks() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := tm.evaluateAllRules(); err != nil {
log.Printf("Failed to evaluate rules: %v", err)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := tm.evaluateAllRules(); err != nil {
// Only log if not a context cancellation or database closed error
if ctx.Err() == nil && !strings.Contains(err.Error(), "database is closed") {
log.Printf("Failed to evaluate rules: %v", err)
}
}
}
}
}()

View File

@@ -179,6 +179,11 @@ func (sm *SecurityManager) ValidateInput() *InputValidator {
return sm.validator
}
// GenerateCSRFToken generates a new CSRF token
func (sm *SecurityManager) GenerateCSRFToken() (string, error) {
return sm.csrfManager.GenerateToken()
}
// LoginHandler handles user login
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {

View File

@@ -0,0 +1,105 @@
package dashboard
import (
"encoding/json"
"net/http"
)
// WebServiceConfig represents a web service configuration
type WebServiceConfig struct {
Enabled bool `json:"enabled"`
Port int `json:"port"`
Name string `json:"name"`
Path string `json:"path"`
TemplateName string `json:"template_name"`
UseHTTPS bool `json:"use_https"`
}
// WebServicesAPI handles web services configuration API endpoints
type WebServicesAPI struct {
configManager interface {
GetWebServices() []WebServiceConfig
SaveWebServices([]WebServiceConfig) error
}
}
// NewWebServicesAPI creates a new web services API instance
func NewWebServicesAPI(configManager interface {
GetWebServices() []WebServiceConfig
SaveWebServices([]WebServiceConfig) error
}) *WebServicesAPI {
return &WebServicesAPI{configManager: configManager}
}
// RegisterRoutes registers the web services API routes
func (wsa *WebServicesAPI) RegisterRoutes(mux *http.ServeMux, sm *SecurityManager) {
// Require admin auth for both GET and POST.
mux.HandleFunc("/api/webservices", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wsa.handleWebServices)))
}
// handleWebServices handles GET (list) and POST (save) operations for web services
func (wsa *WebServicesAPI) handleWebServices(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
wsa.handleGetWebServices(w, r)
case http.MethodPost:
wsa.handleSaveWebServices(w, r)
default:
wsa.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleGetWebServices returns the current web services configuration
func (wsa *WebServicesAPI) handleGetWebServices(w http.ResponseWriter, r *http.Request) {
services := wsa.configManager.GetWebServices()
resp := map[string]interface{}{
"services": services,
"count": len(services),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
// handleSaveWebServices saves the web services configuration
func (wsa *WebServicesAPI) handleSaveWebServices(w http.ResponseWriter, r *http.Request) {
var req struct {
Services []WebServiceConfig `json:"services"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wsa.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Validate services
for i, s := range req.Services {
if s.Name == "" {
wsa.sendJSONError(w, "Service name is required", http.StatusBadRequest)
return
}
if s.Port <= 0 || s.Port > 65535 {
wsa.sendJSONError(w, "Invalid port number", http.StatusBadRequest)
return
}
if s.Path == "" {
req.Services[i].Path = "/"
}
if s.Path[0] != '/' {
req.Services[i].Path = "/" + s.Path
}
}
if err := wsa.configManager.SaveWebServices(req.Services); err != nil {
wsa.sendJSONError(w, "Failed to save web services", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Web services saved successfully"})
}
// sendJSONError sends a JSON error response
func (wsa *WebServicesAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}

View File

@@ -0,0 +1,228 @@
package dashboard
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
)
// WebTemplateAPI exposes endpoints to manage web templates
type WebTemplateAPI struct {
templateManager interface {
GetTemplate(name string) (string, error)
SaveTemplate(name, content string) error
ListTemplates() ([]string, error)
DeleteTemplate(name string) error
ValidateTemplate(content string) error
CreateDefaultTemplate(name string) error
}
}
// NewWebTemplateAPI constructs the API wrapper
func NewWebTemplateAPI(tm interface {
GetTemplate(name string) (string, error)
SaveTemplate(name, content string) error
ListTemplates() ([]string, error)
DeleteTemplate(name string) error
ValidateTemplate(content string) error
CreateDefaultTemplate(name string) error
}) *WebTemplateAPI {
return &WebTemplateAPI{templateManager: tm}
}
// RegisterRoutes registers all endpoints (guarded by security middleware from caller)
func (wta *WebTemplateAPI) RegisterRoutes(mux *http.ServeMux, sm *SecurityManager) {
mux.HandleFunc("/api/webtemplates", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplates)))
mux.HandleFunc("/api/webtemplates/", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplate)))
mux.HandleFunc("/api/webtemplates/validate", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleValidateTemplate)))
}
// handleTemplates supports list and create
func (wta *WebTemplateAPI) handleTemplates(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
wta.handleListTemplates(w, r)
case http.MethodPost:
wta.handleCreateTemplate(w, r)
default:
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleTemplate supports get/update/delete for a specific template name in the path
func (wta *WebTemplateAPI) handleTemplate(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/api/webtemplates/")
if name == "" || name == "/" {
wta.sendJSONError(w, "Template name required", http.StatusBadRequest)
return
}
// Normalize (strip leading slash, append .html if missing)
name = strings.TrimPrefix(strings.TrimSpace(name), "/")
if name != "" && !strings.HasSuffix(strings.ToLower(name), ".html") {
name += ".html"
}
if !wta.isValidTemplateName(name) {
wta.sendJSONError(w, "Invalid template name", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
wta.handleGetTemplate(w, r, name)
case http.MethodPut:
wta.handleUpdateTemplate(w, r, name)
case http.MethodDelete:
wta.handleDeleteTemplate(w, r, name)
default:
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// List
func (wta *WebTemplateAPI) handleListTemplates(w http.ResponseWriter, r *http.Request) {
names, err := wta.templateManager.ListTemplates()
if err != nil {
wta.sendJSONError(w, "Failed to list templates", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"templates": names,
"count": len(names),
})
}
// Get
func (wta *WebTemplateAPI) handleGetTemplate(w http.ResponseWriter, r *http.Request, name string) {
content, err := wta.templateManager.GetTemplate(name)
if err != nil {
wta.sendJSONError(w, "Template not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"name": name, "content": content})
}
// Create
func (wta *WebTemplateAPI) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name != "" && !strings.HasSuffix(strings.ToLower(req.Name), ".html") {
req.Name += ".html"
}
if !wta.isValidTemplateName(req.Name) {
wta.sendJSONError(w, fmt.Sprintf("Invalid template name: %v", req.Name), http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Content) == "" {
if err := wta.templateManager.CreateDefaultTemplate(req.Name); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Failed to create default template: %v", err), http.StatusInternalServerError)
return
}
} else {
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
return
}
if err := wta.templateManager.SaveTemplate(req.Name, req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Failed to save template: %v", err), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template created successfully", "name": req.Name})
}
// Update
func (wta *WebTemplateAPI) handleUpdateTemplate(w http.ResponseWriter, r *http.Request, name string) {
var req struct{ Content string `json:"content"` }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
return
}
if err := wta.templateManager.SaveTemplate(name, req.Content); err != nil {
wta.sendJSONError(w, "Failed to update template", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template updated successfully", "name": name})
}
// Delete
func (wta *WebTemplateAPI) handleDeleteTemplate(w http.ResponseWriter, r *http.Request, name string) {
if err := wta.templateManager.DeleteTemplate(name); err != nil {
wta.sendJSONError(w, "Failed to delete template", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template deleted successfully", "name": name})
}
// Validate (content only)
func (wta *WebTemplateAPI) handleValidateTemplate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct{ Content string `json:"content"` }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"valid": false, "error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"valid": true})
}
// Validation helper for names
func (wta *WebTemplateAPI) isValidTemplateName(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return false
}
if !strings.HasSuffix(strings.ToLower(name), ".html") {
return false
}
// Disallow path separators and traversal
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
return false
}
// Must be a base filename
if name != filepath.Base(name) {
return false
}
if len(name) > 100 {
return false
}
// Only safe characters
for i := 0; i < len(name); i++ {
c := name[i]
if !(c == '-' || c == '_' || c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
return false
}
}
return true
}
func (wta *WebTemplateAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}

View File

@@ -0,0 +1,220 @@
package dashboard
import (
"fmt"
"html/template"
"os"
"path/filepath"
"strings"
)
// WebTemplateManager manages HTML templates for web honeypot services
type WebTemplateManager struct {
templateDir string
}
// NewWebTemplateManager creates a new web template manager
func NewWebTemplateManager(templateDir string) *WebTemplateManager {
// Ensure template directory exists
if err := os.MkdirAll(templateDir, 0755); err != nil {
// Log error but continue - directory will be created when first template is saved
}
return &WebTemplateManager{
templateDir: templateDir,
}
}
// GetTemplate retrieves a template by name
func (wtm *WebTemplateManager) GetTemplate(templateName string) (string, error) {
if !wtm.isValidTemplateName(templateName) {
return "", fmt.Errorf("invalid template name: %s", templateName)
}
templatePath := filepath.Join(wtm.templateDir, templateName)
content, err := os.ReadFile(templatePath)
if err != nil {
return "", fmt.Errorf("failed to read template %s: %w", templateName, err)
}
return string(content), nil
}
// SaveTemplate saves a template with the given name and content
func (wtm *WebTemplateManager) SaveTemplate(templateName, content string) error {
if !wtm.isValidTemplateName(templateName) {
return fmt.Errorf("invalid template name: %s", templateName)
}
// Ensure template directory exists
if err := os.MkdirAll(wtm.templateDir, 0755); err != nil {
return fmt.Errorf("failed to create template directory: %w", err)
}
templatePath := filepath.Join(wtm.templateDir, templateName)
if err := os.WriteFile(templatePath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to save template %s: %w", templateName, err)
}
return nil
}
// ListTemplates returns a list of available template names
func (wtm *WebTemplateManager) ListTemplates() ([]string, error) {
if _, err := os.Stat(wtm.templateDir); os.IsNotExist(err) {
return []string{}, nil
}
entries, err := os.ReadDir(wtm.templateDir)
if err != nil {
return nil, fmt.Errorf("failed to read template directory: %w", err)
}
var templates []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
templates = append(templates, entry.Name())
}
}
return templates, nil
}
// DeleteTemplate removes a template
func (wtm *WebTemplateManager) DeleteTemplate(templateName string) error {
if !wtm.isValidTemplateName(templateName) {
return fmt.Errorf("invalid template name: %s", templateName)
}
templatePath := filepath.Join(wtm.templateDir, templateName)
if err := os.Remove(templatePath); err != nil {
return fmt.Errorf("failed to delete template %s: %w", templateName, err)
}
return nil
}
// ValidateTemplate validates template content for basic HTML structure
func (wtm *WebTemplateManager) ValidateTemplate(content string) error {
// Basic validation - check if it's valid HTML template
_, err := template.New("test").Parse(content)
if err != nil {
return fmt.Errorf("invalid template syntax: %w", err)
}
// Additional validation - ensure it contains basic HTML structure
content = strings.ToLower(content)
if !strings.Contains(content, "<html") {
return fmt.Errorf("template must contain <html> tag")
}
if !strings.Contains(content, "<body") {
return fmt.Errorf("template must contain <body> tag")
}
return nil
}
// CreateDefaultTemplate creates a default template with the given name
func (wtm *WebTemplateManager) CreateDefaultTemplate(templateName string) error {
if !wtm.isValidTemplateName(templateName) {
return fmt.Errorf("invalid template name: %s", templateName)
}
defaultContent := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - {{ .ServiceName }}</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
margin: 0;
padding: 20px;
}
.login-container {
max-width: 400px;
margin: 50px auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
.error {
color: red;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<h2>{{ .ServiceName }}</h2>
<form method="POST" action="{{ .LoginPath }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>`
return wtm.SaveTemplate(templateName, defaultContent)
}
// isValidTemplateName validates template name for security
func (wtm *WebTemplateManager) isValidTemplateName(name string) bool {
name = strings.TrimSpace(name)
// Must end with .html
if !strings.HasSuffix(strings.ToLower(name), ".html") {
return false
}
// Disallow path separators and traversal, but allow single dots
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
return false
}
// Must not be empty or too long
if len(name) == 0 || len(name) > 100 {
return false
}
// Must be a valid filename
if name != filepath.Base(name) {
return false
}
return true
}

View File

@@ -22,20 +22,23 @@ import (
"sync"
"time"
"golang.org/x/crypto/ssh"
"honeydany/app/dashboard"
svcs "honeydany/app/services"
"golang.org/x/crypto/ssh"
)
// App holds runtime pieces
type App struct {
cfg Config
logger *Logger
threatIntel *ThreatIntel
threatManager *dashboard.ThreatManager
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
cfg Config
logger *Logger
threatIntel *ThreatIntel
threatManager *dashboard.ThreatManager
webTemplateManager *WebTemplateManager
webServiceManager *WebServiceManager
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// keep references to servers for graceful shutdown
httpSrvs []*http.Server
sshSigner ssh.Signer
@@ -64,9 +67,59 @@ func (a *App) startHTTPS(port int) {
if sni := r.TLS; sni != nil && sni.ServerName != "" {
details["sni_server_name"] = sni.ServerName
}
// Handle POST requests (login attempts)
if r.Method == http.MethodPost {
if err := r.ParseForm(); err == nil {
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
if username != "" || password != "" {
details["login_attempt"] = "true"
details["username"] = username
details["password"] = password
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
// Log the login attempt
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
a.logEvent(rec)
// Redirect back with error (honeypot always rejects)
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
}
}
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
a.logEvent(rec)
// Check if template is configured
if a.cfg.Web.HTTPSTemplateName != "" && a.webTemplateManager != nil {
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPSTemplateName)
if err == nil {
// Render template with context data compatible with admin-login.html
data := map[string]interface{}{
"ServiceName": "HTTPS Service",
"LoginPath": r.URL.Path,
"UseHTTPS": true,
}
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
if err := tmpl.Execute(w, data); err != nil {
log.Printf("HTTPS template execution error: %v", err)
_, _ = w.Write([]byte("Welcome (TLS)\n"))
}
return
} else {
log.Printf("HTTPS template load error: %v", err)
}
}
// Default response if no template or template failed
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
@@ -216,15 +269,53 @@ func NewApp(cfg Config) (*App, error) {
tm = nil // Continue without threat manager if it fails
}
// Initialize web template manager
configPath := "config.json" // Default path, should be passed from main
if LastConfigPath != "" {
configPath = LastConfigPath
}
webTemplateManager := NewWebTemplateManager(configPath)
// Initialize web service manager
webServiceManager := NewWebServiceManager(webTemplateManager, func(record Record) {
// Use the same logging function as other services
if l != nil {
_ = l.Log(record)
}
// Also process with threat intelligence
if ti != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
ti.RecordActivity(record)
}
// Process with threat manager
if tm != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
details := make(map[string]interface{})
for k, v := range record.Details {
details[k] = v
}
tm.ProcessHoneypotRecord(record.Timestamp, record.RemoteAddr, record.RemotePort, record.Service, details, record.RawPayload)
}
})
// Root context for the App used for shutdown signalling
ctx, cancel := context.WithCancel(context.Background())
a := &App{cfg: cfg, logger: l, threatIntel: ti, threatManager: tm, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1)}
a := &App{
cfg: cfg,
logger: l,
threatIntel: ti,
threatManager: tm,
webTemplateManager: webTemplateManager,
webServiceManager: webServiceManager,
ctx: ctx,
cancel: cancel,
conns: make(map[net.Conn]struct{}),
restartCh: make(chan struct{}, 1),
}
// Start periodic threat analysis tasks
if tm != nil {
tm.RunPeriodicTasks()
tm.RunPeriodicTasks(a.ctx)
}
return a, nil
}
@@ -322,14 +413,7 @@ func (a *App) Run(ctx context.Context) error {
a.startTCPService("vnc", a.cfg.Ports.VNC, svcs.NewVNCHandler(a.svcLogger()))
}()
}
for _, p := range a.cfg.Services.Generic {
port := p
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService(fmt.Sprintf("generic-%d", port), port, svcs.NewGenericEchoHandler(a.svcLogger()))
}()
}
// start web dashboard if enabled
if a.cfg.Web.Enabled {
a.wg.Add(1)
@@ -339,6 +423,15 @@ func (a *App) Run(ctx context.Context) error {
}()
}
// Start custom web services (only generic services)
for _, genericService := range a.cfg.WebServices.Generic {
if genericService.Enabled {
if err := a.webServiceManager.StartWebService(genericService); err != nil {
log.Printf("Failed to start web service %s: %v", genericService.Name, err)
}
}
}
<-ctx.Done()
a.Shutdown()
return nil
@@ -482,9 +575,59 @@ func (a *App) startHTTP(port int) {
if auth := r.Header.Get("Authorization"); auth != "" {
details["authorization"] = auth
}
// Handle POST requests (login attempts)
if r.Method == http.MethodPost {
if err := r.ParseForm(); err == nil {
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
if username != "" || password != "" {
details["login_attempt"] = "true"
details["username"] = username
details["password"] = password
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
// Log the login attempt
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
a.logEvent(rec)
// Redirect back with error (honeypot always rejects)
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
}
}
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
a.logEvent(rec)
// Check if template is configured
if a.cfg.Web.HTTPTemplateName != "" && a.webTemplateManager != nil {
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPTemplateName)
if err == nil {
// Render template with context data compatible with admin-login.html
data := map[string]interface{}{
"ServiceName": "HTTP Service",
"LoginPath": r.URL.Path,
"UseHTTPS": false,
}
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
if err := tmpl.Execute(w, data); err != nil {
log.Printf("HTTP template execution error: %v", err)
_, _ = w.Write([]byte("Welcome\n"))
}
return
} else {
log.Printf("HTTP template load error: %v", err)
}
}
// Default response if no template or template failed
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)

View File

@@ -69,6 +69,8 @@
{{ template "threat_reports_content" . }}
{{ else if eq .PageContent "threat_rules_content" }}
{{ template "threat_rules_content" . }}
{{ else if eq .PageContent "webtemplates_content" }}
{{ template "webtemplates_content" . }}
{{ else if eq .PageContent "users_content" }}
{{ template "users_content" . }}
{{ end }}

View File

@@ -127,13 +127,63 @@
</label>
</div>
</div>
<!-- HTTP/HTTPS Templates Section -->
<div class="bg-gray-800 border border-gray-700 rounded-lg p-6">
<h2 class="text-lg font-semibold text-white mb-4">HTTP/HTTPS Templates</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label class="block">
<span class="text-gray-300">HTTP Template</span>
<select id="http-template" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" data-current-value="{{ .Cfg.Web.HTTPTemplateName }}">
<option value="">None (Default Welcome)</option>
<!-- Templates will be loaded here -->
</select>
</label>
<label class="block">
<span class="text-gray-300">HTTPS Template</span>
<select id="https-template" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" data-current-value="{{ .Cfg.Web.HTTPSTemplateName }}">
<option value="">None (Default Welcome)</option>
<!-- Templates will be loaded here -->
</select>
</label>
</div>
</div>
</div>
<!-- Web Services Section -->
<div class="mt-6 bg-gray-800 border border-gray-700 rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-white">Web Services</h2>
<button id="btn-add-webservice" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">
Add Service
</button>
</div>
<div id="webservices-list" class="space-y-4">
<!-- Web services will be loaded here -->
</div>
</div>
<!-- Web Templates Section -->
<div class="mt-6 bg-gray-800 border border-gray-700 rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-white">Web Templates</h2>
<button id="btn-manage-templates" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm">
Manage Templates
</button>
</div>
<div id="templates-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<!-- Templates will be loaded here -->
</div>
</div>
<div class="mt-6 flex items-center gap-3">
<button id="btn-save" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">Save Settings</button>
<button id="btn-restart" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">Restart App</button>
<span id="save-status" class="text-sm text-gray-400"></span>
</div>
<script>
let currentCSRFToken = '{{ .CSRFToken }}';
async function saveSettings() {
const payload = {
services: {
@@ -167,17 +217,24 @@
smb: parseInt(document.getElementById('port-smb').value, 10),
sip: parseInt(document.getElementById('port-sip').value, 10),
vnc: parseInt(document.getElementById('port-vnc').value, 10),
},
web: {
http_template_name: document.getElementById('http-template').value,
https_template_name: document.getElementById('https-template').value,
}
};
const headers = { 'Content-Type': 'application/json' };
const csrfToken = '{{ .CSRFToken }}';
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
if (currentCSRFToken) {
headers['X-CSRF-Token'] = currentCSRFToken;
}
const res = await fetch('/api/settings', { method: 'POST', headers: headers, body: JSON.stringify(payload) });
const out = await res.json().catch(() => ({}));
const el = document.getElementById('save-status');
if (res.ok) {
// Update CSRF token for subsequent requests
if (out.csrf_token) {
currentCSRFToken = out.csrf_token;
}
el.textContent = 'Saved. You may need to restart to apply port changes.';
el.className = 'text-sm text-green-400';
} else {
@@ -187,12 +244,16 @@
}
async function restartApp() {
const headers = {};
const csrfToken = '{{ .CSRFToken }}';
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
if (currentCSRFToken) {
headers['X-CSRF-Token'] = currentCSRFToken;
}
const res = await fetch('/api/restart', { method: 'POST', headers: headers });
const out = await res.json().catch(() => ({}));
if (res.ok) {
// Update CSRF token for subsequent requests
if (out.csrf_token) {
currentCSRFToken = out.csrf_token;
}
document.getElementById('save-status').textContent = 'Restarting...';
setTimeout(() => location.reload(), 1200);
} else {
@@ -202,5 +263,272 @@
}
document.getElementById('btn-save').addEventListener('click', saveSettings);
document.getElementById('btn-restart').addEventListener('click', restartApp);
// Web Templates Management
document.getElementById('btn-manage-templates').addEventListener('click', () => {
openTemplateManager();
});
// Web Services Management
document.getElementById('btn-add-webservice').addEventListener('click', () => {
addWebService();
});
// Load initial data
loadWebServices();
loadTemplates();
async function loadWebServices() {
try {
console.log('Loading web services from server...');
const response = await fetch('/api/webservices');
const data = await response.json();
console.log('Loaded web services data:', data);
webServices = data.services || [];
console.log('webServices array after load:', webServices);
renderWebServices(webServices);
} catch (error) {
console.error('Failed to load web services:', error);
webServices = [];
}
}
async function loadTemplates() {
try {
const response = await fetch('/api/webtemplates');
const data = await response.json();
renderTemplates(data.templates || []);
} catch (error) {
console.error('Failed to load templates:', error);
}
}
function renderWebServices(services) {
const container = document.getElementById('webservices-list');
if (services.length === 0) {
container.innerHTML = '<p class="text-gray-400 text-center py-4">No web services configured</p>';
return;
}
// Update the global webServices array to keep it in sync
webServices = [...services];
container.innerHTML = services.map((service, index) => `
<div class="bg-gray-900 border border-gray-600 rounded p-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 items-center">
<div>
<label class="block text-sm text-gray-300 mb-1">Name</label>
<input type="text" value="${service.name}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
onchange="updateWebService(${index}, 'name', this.value)">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Port</label>
<input type="number" value="${service.port}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
onchange="updateWebService(${index}, 'port', parseInt(this.value))">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Path</label>
<input type="text" value="${service.path}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
onchange="updateWebService(${index}, 'path', this.value)">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Template</label>
<select class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
onchange="updateWebService(${index}, 'template_name', this.value)">
<option value="">None (200 OK)</option>
${getTemplateOptions(service.template_name)}
</select>
</div>
<div class="flex items-center gap-2">
<label class="flex items-center">
<input type="checkbox" ${service.enabled ? 'checked' : ''} class="mr-1"
onchange="updateWebService(${index}, 'enabled', this.checked)">
<span class="text-sm text-gray-300">Enabled</span>
</label>
<button onclick="removeWebService(${index})" class="text-red-400 hover:text-red-300 text-sm">Remove</button>
</div>
</div>
</div>
`).join('');
}
function getTemplateOptions(selectedTemplate) {
if (!window.availableTemplates || window.availableTemplates.length === 0) {
return '<option value="" disabled>Loading templates...</option>';
}
return window.availableTemplates.map(t =>
`<option value="${t}" ${selectedTemplate === t ? 'selected' : ''}>${t}</option>`
).join('');
}
function renderTemplates(templates) {
window.availableTemplates = templates;
const container = document.getElementById('templates-list');
if (templates.length === 0) {
container.innerHTML = '<p class="text-gray-400 text-center py-4 col-span-full">No templates found</p>';
} else {
container.innerHTML = templates.map(template => `
<div class="bg-gray-900 border border-gray-600 rounded p-3">
<div class="flex items-center justify-between">
<span class="text-gray-100 text-sm font-medium">${template}</span>
<button onclick="editTemplate('${template}')" class="text-blue-400 hover:text-blue-300 text-xs">Edit</button>
</div>
</div>
`).join('');
}
// Update HTTP/HTTPS template dropdowns
updateTemplateDropdowns(templates);
// Re-render web services to update their template dropdowns
if (webServices && webServices.length > 0) {
renderWebServices(webServices);
}
}
function updateTemplateDropdowns(templates) {
const httpSelect = document.getElementById('http-template');
const httpsSelect = document.getElementById('https-template');
// Get current values from data attributes or current selection
const currentHttpTemplate = httpSelect.dataset.currentValue || httpSelect.value;
const currentHttpsTemplate = httpsSelect.dataset.currentValue || httpsSelect.value;
// Clear and repopulate options
[httpSelect, httpsSelect].forEach(select => {
// Keep the "None" option
select.innerHTML = '<option value="">None (Default Welcome)</option>';
templates.forEach(template => {
const option = document.createElement('option');
option.value = template;
option.textContent = template;
select.appendChild(option);
});
});
// Set current values
httpSelect.value = currentHttpTemplate;
httpsSelect.value = currentHttpsTemplate;
// Clear data attributes after first use
delete httpSelect.dataset.currentValue;
delete httpsSelect.dataset.currentValue;
}
let webServices = [];
function addWebService() {
const newService = {
enabled: false,
port: 9000 + webServices.length,
name: `service-${webServices.length + 1}`,
path: '/login',
template_name: '',
use_https: false
};
webServices.push(newService);
renderWebServices(webServices);
}
async function updateWebService(index, field, value) {
if (webServices[index]) {
webServices[index][field] = value;
// Auto-save web services when changed
await saveWebServices();
}
}
async function saveWebServices() {
try {
console.log('Saving web services:', webServices);
const headers = { 'Content-Type': 'application/json' };
if (currentCSRFToken) {
headers['X-CSRF-Token'] = currentCSRFToken;
}
const payload = { services: webServices };
console.log('Payload being sent:', payload);
const response = await fetch('/api/webservices', {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
const result = await response.json();
console.log('Server response:', result);
if (response.ok) {
// Update CSRF token if provided
if (result.csrf_token) {
currentCSRFToken = result.csrf_token;
}
console.log('Web services saved successfully');
// Restart app so changes (like template swap) take effect on running web services
try {
const hdrs = {};
if (currentCSRFToken) hdrs['X-CSRF-Token'] = currentCSRFToken;
const rres = await fetch('/api/restart', { method: 'POST', headers: hdrs });
if (rres.ok) {
console.log('Restart triggered');
} else {
console.warn('Restart request failed');
}
} catch (e) {
console.warn('Error triggering restart', e);
}
} else {
console.error('Failed to save web services:', result.error);
}
} catch (error) {
console.error('Error saving web services:', error);
}
}
async function removeWebService(index) {
webServices.splice(index, 1);
console.log('After removal, webServices:', webServices);
// Handle empty services array
if (webServices.length === 0) {
document.getElementById('webservices-list').innerHTML = '<p class="text-gray-400 text-center py-4">No web services configured</p>';
} else {
renderWebServices(webServices);
}
// Auto-save after removal
await saveWebServices();
}
function openTemplateManager() {
// Open template manager modal (we'll embed the template manager here)
showTemplateManagerModal();
}
function editTemplate(templateName) {
showTemplateManagerModal(templateName);
}
function showTemplateManagerModal(templateName = null) {
// Create and show the template manager modal
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="bg-gray-800 rounded-lg border border-gray-700 w-full max-w-6xl h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-700 flex justify-between items-center shrink-0">
<h3 class="text-lg font-semibold text-gray-100">Template Manager</h3>
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-200">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex-1 p-4 overflow-hidden">
<iframe src="/webtemplates" class="w-full h-full min-h-0 border-0 rounded"></iframe>
</div>
</div>
`;
document.body.appendChild(modal);
}
</script>
{{ end }}

View File

@@ -0,0 +1,382 @@
{{ define "webtemplates_content" }}
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-100">Web Templates</h1>
<p class="text-gray-400 mt-1">Manage HTML templates for web honeypot services</p>
</div>
<button id="btn-new-template" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium">
New Template
</button>
</div>
<!-- Template List -->
<div class="bg-gray-800 rounded-lg border border-gray-700">
<div class="p-6">
<h2 class="text-lg font-semibold text-gray-100 mb-4">Available Templates</h2>
<div id="template-list" class="space-y-3">
<!-- Templates will be loaded here -->
</div>
</div>
</div>
<!-- Template Editor Modal -->
<div id="template-editor-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-gray-800 rounded-lg border border-gray-700 w-full max-w-4xl max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-gray-700">
<div class="flex justify-between items-center">
<h3 id="editor-title" class="text-lg font-semibold text-gray-100">Edit Template</h3>
<button id="btn-close-editor" class="text-gray-400 hover:text-gray-200">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
<!-- Editor -->
<div class="flex flex-col">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-300">Template Name</label>
<button id="btn-validate" class="text-sm bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded">
Validate
</button>
</div>
<input id="template-name" type="text" placeholder="template-name.html"
class="mb-3 bg-gray-900 border border-gray-600 rounded px-3 py-2 text-gray-100 text-sm">
<label class="text-sm font-medium text-gray-300 mb-2">HTML Content</label>
<textarea id="template-content"
class="flex-1 bg-gray-900 border border-gray-600 rounded px-3 py-2 text-gray-100 font-mono text-sm resize-none min-h-96"
placeholder="Enter your HTML template here..."
spellcheck="false"
style="tab-size: 2; white-space: pre; overflow-wrap: normal; overflow-x: auto;"></textarea>
<div id="validation-result" class="mt-2 text-sm hidden"></div>
</div>
<!-- Preview -->
<div class="flex flex-col">
<label class="text-sm font-medium text-gray-300 mb-2">Preview</label>
<div class="flex-1 border border-gray-600 rounded overflow-hidden">
<iframe id="template-preview" class="w-full h-full bg-white"></iframe>
</div>
</div>
</div>
</div>
<div class="p-6 border-t border-gray-700 flex justify-between">
<div class="space-x-2">
<button id="btn-preview" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
Update Preview
</button>
<button id="btn-create-default" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded">
Load Default Template
</button>
</div>
<div class="space-x-2">
<button id="btn-cancel-edit" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
Cancel
</button>
<button id="btn-save-template" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
Save Template
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Status Messages -->
<div id="status-message" class="hidden fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50"></div>
</div>
<script>
let currentTemplate = null;
let templates = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadTemplates();
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('btn-new-template').addEventListener('click', () => openEditor());
document.getElementById('btn-close-editor').addEventListener('click', closeEditor);
document.getElementById('btn-cancel-edit').addEventListener('click', closeEditor);
document.getElementById('btn-save-template').addEventListener('click', saveTemplate);
document.getElementById('btn-validate').addEventListener('click', validateTemplate);
document.getElementById('btn-preview').addEventListener('click', updatePreview);
document.getElementById('btn-create-default').addEventListener('click', loadDefaultTemplate);
}
async function loadTemplates() {
try {
const response = await fetch('/api/webtemplates');
const data = await response.json();
if (response.ok) {
templates = data.templates || [];
renderTemplateList();
} else {
showStatus('Failed to load templates: ' + data.error, 'error');
}
} catch (error) {
showStatus('Error loading templates: ' + error.message, 'error');
}
}
function renderTemplateList() {
const container = document.getElementById('template-list');
if (templates.length === 0) {
container.innerHTML = '<p class="text-gray-400 text-center py-8">No templates found. Create your first template!</p>';
return;
}
container.innerHTML = templates.map(template => `
<div class="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-600">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span class="text-gray-100 font-medium">${template}</span>
</div>
<div class="space-x-2">
<button onclick="editTemplate('${template}')" class="text-blue-400 hover:text-blue-300 text-sm">
Edit
</button>
<button onclick="deleteTemplate('${template}')" class="text-red-400 hover:text-red-300 text-sm">
Delete
</button>
</div>
</div>
`).join('');
}
function openEditor(templateName = null) {
currentTemplate = templateName;
if (templateName) {
document.getElementById('editor-title').textContent = 'Edit Template';
document.getElementById('template-name').value = templateName;
document.getElementById('template-name').disabled = true;
loadTemplateContent(templateName);
} else {
document.getElementById('editor-title').textContent = 'New Template';
document.getElementById('template-name').value = '';
document.getElementById('template-name').disabled = false;
document.getElementById('template-content').value = '';
}
document.getElementById('template-editor-modal').classList.remove('hidden');
}
function closeEditor() {
document.getElementById('template-editor-modal').classList.add('hidden');
currentTemplate = null;
}
async function loadTemplateContent(templateName) {
try {
const response = await fetch(`/api/webtemplates/${templateName}`);
const data = await response.json();
if (response.ok) {
document.getElementById('template-content').value = data.content;
updatePreview();
} else {
showStatus('Failed to load template: ' + data.error, 'error');
}
} catch (error) {
showStatus('Error loading template: ' + error.message, 'error');
}
}
async function saveTemplate() {
let name = document.getElementById('template-name').value.trim();
const content = document.getElementById('template-content').value;
if (!name) {
showStatus('Template name is required', 'error');
return;
}
// Automatically add .html extension if not present
if (!name.endsWith('.html')) {
name = name + '.html';
document.getElementById('template-name').value = name;
}
try {
const url = currentTemplate ? `/api/webtemplates/${currentTemplate}` : '/api/webtemplates';
const method = currentTemplate ? 'PUT' : 'POST';
const headers = { 'Content-Type': 'application/json' };
const csrfToken = '{{ .CSRFToken }}';
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
const body = currentTemplate ?
JSON.stringify({ content: content }) :
JSON.stringify({ name: name, content: content });
const response = await fetch(url, {
method: method,
headers: headers,
body: body
});
const data = await response.json();
if (response.ok) {
showStatus('Template saved successfully', 'success');
closeEditor();
loadTemplates();
} else {
showStatus('Failed to save template: ' + data.error, 'error');
}
} catch (error) {
showStatus('Error saving template: ' + error.message, 'error');
}
}
async function validateTemplate() {
const content = document.getElementById('template-content').value;
if (!content.trim()) {
showValidationResult(false, 'Template content is empty');
return;
}
try {
const headers = { 'Content-Type': 'application/json' };
const csrfToken = '{{ .CSRFToken }}';
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
const response = await fetch('/api/webtemplates/validate', {
method: 'POST',
headers: headers,
body: JSON.stringify({ content: content })
});
const data = await response.json();
showValidationResult(data.valid, data.error || 'Template is valid');
} catch (error) {
showValidationResult(false, 'Error validating template: ' + error.message);
}
}
function showValidationResult(isValid, message) {
const resultDiv = document.getElementById('validation-result');
resultDiv.className = `mt-2 text-sm ${isValid ? 'text-green-400' : 'text-red-400'}`;
resultDiv.textContent = message;
resultDiv.classList.remove('hidden');
}
function updatePreview() {
const content = document.getElementById('template-content').value;
const preview = document.getElementById('template-preview');
// Create a blob URL for the preview
const blob = new Blob([content], { type: 'text/html' });
const url = URL.createObjectURL(blob);
preview.src = url;
}
function loadDefaultTemplate() {
const defaultTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - {{ .ServiceName }}</title>
<style>
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
.login-container { max-width: 400px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background: #0056b3; }
.error { color: red; margin-top: 10px; }
</style>
</head>
<body>
<div class="login-container">
<h2>{{ .ServiceName }}</h2>
<form method="POST" action="{{ .LoginPath }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>`;
document.getElementById('template-content').value = defaultTemplate;
updatePreview();
}
async function editTemplate(templateName) {
openEditor(templateName);
}
async function deleteTemplate(templateName) {
if (!confirm(`Are you sure you want to delete the template "${templateName}"?`)) {
return;
}
try {
const headers = {};
const csrfToken = '{{ .CSRFToken }}';
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
const response = await fetch(`/api/webtemplates/${templateName}`, {
method: 'DELETE',
headers: headers
});
const data = await response.json();
if (response.ok) {
showStatus('Template deleted successfully', 'success');
loadTemplates();
} else {
showStatus('Failed to delete template: ' + data.error, 'error');
}
} catch (error) {
showStatus('Error deleting template: ' + error.message, 'error');
}
}
function showStatus(message, type) {
const statusDiv = document.getElementById('status-message');
statusDiv.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-600' : 'bg-red-600'
}`;
statusDiv.textContent = message;
statusDiv.classList.remove('hidden');
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 5000);
}
</script>
{{ end }}

View File

@@ -38,6 +38,7 @@ func initTemplates() error {
"templates/settings.html",
"templates/threat_reports.html",
"templates/threat_rules.html",
"templates/webtemplates.html",
"templates/users.html",
)
if err != nil { return err }
@@ -71,6 +72,12 @@ func (a *App) startWeb() {
// Register blocklist export routes (public endpoints for threat intelligence sharing)
a.threatManager.GetBlocklistExporter().RegisterExportRoutes(mux, securityManager)
// Register web template management routes (admin only)
a.threatManager.GetWebTemplateAPI().RegisterRoutes(mux, securityManager)
// Register web services management routes (admin only)
a.threatManager.GetWebServicesAPI().RegisterRoutes(mux, securityManager)
}
// Secure dashboard routes with authentication
@@ -176,6 +183,10 @@ func (a *App) startWeb() {
SIP int `json:"sip"`
VNC int `json:"vnc"`
} `json:"ports"`
Web struct {
HTTPTemplateName string `json:"http_template_name"`
HTTPSTemplateName string `json:"https_template_name"`
} `json:"web"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
w.WriteHeader(http.StatusBadRequest)
@@ -213,11 +224,24 @@ func (a *App) startWeb() {
a.cfg.Ports.SIP = in.Ports.SIP
a.cfg.Ports.VNC = in.Ports.VNC
// Update web template settings
a.cfg.Web.HTTPTemplateName = in.Web.HTTPTemplateName
a.cfg.Web.HTTPSTemplateName = in.Web.HTTPSTemplateName
// Persist to ./config.json
if b, err := json.MarshalIndent(a.cfg, "", " "); err == nil {
_ = os.WriteFile("config.json", b, 0644)
}
_ = json.NewEncoder(w).Encode(map[string]string{"status":"ok"})
// Generate new CSRF token for subsequent requests
response := map[string]string{"status": "ok"}
if a.threatManager != nil {
if newToken, err := a.threatManager.GetSecurityManager().GenerateCSRFToken(); err == nil {
response["csrf_token"] = newToken
w.Header().Set("X-CSRF-Token", newToken)
}
}
_ = json.NewEncoder(w).Encode(response)
return
default:
w.WriteHeader(http.StatusMethodNotAllowed)
@@ -227,7 +251,16 @@ func (a *App) startWeb() {
// Restart endpoint: triggers app restart
mux.HandleFunc("/api/restart", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return }
_ = json.NewEncoder(w).Encode(map[string]string{"status":"restarting"})
// Generate new CSRF token for subsequent requests
response := map[string]string{"status": "restarting"}
if a.threatManager != nil {
if newToken, err := a.threatManager.GetSecurityManager().GenerateCSRFToken(); err == nil {
response["csrf_token"] = newToken
w.Header().Set("X-CSRF-Token", newToken)
}
}
_ = json.NewEncoder(w).Encode(response)
go func(){ time.Sleep(700*time.Millisecond); a.Restart() }()
})))
mux.HandleFunc("/logs", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
@@ -374,7 +407,7 @@ func (a *App) startWeb() {
http.Error(w, "templates not loaded", 500)
}))
// Users page (Admin only)
// Role middleware setup for admin pages
var roleMiddleware func(string) func(http.HandlerFunc) http.HandlerFunc
if a.threatManager != nil {
roleMiddleware = a.threatManager.GetSecurityManager().RoleMiddleware
@@ -385,6 +418,24 @@ func (a *App) startWeb() {
}
}
// Web Templates page (Admin only)
mux.HandleFunc("/webtemplates", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"PageTitle": "webtemplates_title",
"PageContent": "webtemplates_content",
}
// Add CSRF token if security manager is available
if a.threatManager != nil {
a.threatManager.GetSecurityManager().AddCSRFToken(w, data)
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})))
mux.HandleFunc("/users", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) {
// Get current user from context
var currentUser interface{}

241
app/webservice_handler.go Normal file
View File

@@ -0,0 +1,241 @@
package app
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
)
// WebServiceHandler handles custom web honeypot services
type WebServiceHandler struct {
config WebServiceConfig
templateManager *WebTemplateManager
logFunc func(Record)
}
// NewWebServiceHandler creates a new web service handler
func NewWebServiceHandler(config WebServiceConfig, templateManager *WebTemplateManager, logFunc func(Record)) *WebServiceHandler {
return &WebServiceHandler{
config: config,
templateManager: templateManager,
logFunc: logFunc,
}
}
// ServeHTTP implements the http.Handler interface
func (wsh *WebServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Log all requests
wsh.logRequest(r)
// Check if request is for the configured path
if r.URL.Path == wsh.config.Path {
wsh.handleLoginPage(w, r)
return
}
// For all other paths, return 403 Forbidden
wsh.logEvent(r, "forbidden_access", map[string]string{
"requested_path": r.URL.Path,
"configured_path": wsh.config.Path,
})
http.Error(w, "Forbidden", http.StatusForbidden)
}
// handleLoginPage serves the login page and processes login attempts
func (wsh *WebServiceHandler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
wsh.serveLoginForm(w, r)
case http.MethodPost:
wsh.handleLoginAttempt(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// serveLoginForm renders and serves the login form
func (wsh *WebServiceHandler) serveLoginForm(w http.ResponseWriter, r *http.Request) {
// Load template
tmpl, err := wsh.templateManager.LoadTemplate(wsh.config.TemplateName)
if err != nil {
log.Printf("Failed to load template %s: %v", wsh.config.TemplateName, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Prepare template data
data := map[string]interface{}{
"ServiceName": wsh.config.Name,
"LoginPath": wsh.config.Path,
"UseHTTPS": wsh.config.UseHTTPS,
}
// Set content type
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// Execute template
if err := tmpl.Execute(w, data); err != nil {
log.Printf("Failed to execute template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Log page view
wsh.logEvent(r, "login_page_view", map[string]string{
"template": wsh.config.TemplateName,
})
}
// handleLoginAttempt processes login form submissions
func (wsh *WebServiceHandler) handleLoginAttempt(w http.ResponseWriter, r *http.Request) {
// Parse form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
// Log the login attempt
wsh.logEvent(r, "login_attempt", map[string]string{
"username": username,
"password": password,
"template": wsh.config.TemplateName,
})
// Always reject login attempts (this is a honeypot)
// Redirect back to login page with error parameter
redirectURL := fmt.Sprintf("%s?error=invalid", wsh.config.Path)
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
// logRequest logs basic request information
func (wsh *WebServiceHandler) logRequest(r *http.Request) {
details := map[string]string{
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"user_agent": r.Header.Get("User-Agent"),
"referer": r.Header.Get("Referer"),
}
// Add additional headers that might be interesting
if auth := r.Header.Get("Authorization"); auth != "" {
details["authorization"] = auth
}
if cookie := r.Header.Get("Cookie"); cookie != "" {
details["cookie"] = cookie
}
wsh.logEvent(r, "http_request", details)
}
// logEvent logs an event with the web service context
func (wsh *WebServiceHandler) logEvent(r *http.Request, eventType string, details map[string]string) {
if details == nil {
details = make(map[string]string)
}
// Add service context
details["event"] = eventType
details["service_name"] = wsh.config.Name
details["service_port"] = strconv.Itoa(wsh.config.Port)
details["service_path"] = wsh.config.Path
// Create log record
record := Record{
Timestamp: time.Now().UTC(),
RemoteAddr: remoteIP(r.RemoteAddr),
RemotePort: remotePort(r.RemoteAddr),
Service: fmt.Sprintf("web-%s", wsh.config.Name),
Details: details,
RawPayload: fmt.Sprintf("%s %s", r.Method, r.URL.String()),
}
if wsh.logFunc != nil {
wsh.logFunc(record)
}
}
// WebServiceManager manages multiple web service instances
type WebServiceManager struct {
templateManager *WebTemplateManager
logFunc func(Record)
services map[string]*WebServiceHandler
}
// NewWebServiceManager creates a new web service manager
func NewWebServiceManager(templateManager *WebTemplateManager, logFunc func(Record)) *WebServiceManager {
return &WebServiceManager{
templateManager: templateManager,
logFunc: logFunc,
services: make(map[string]*WebServiceHandler),
}
}
// StartWebService starts a web service on the specified configuration
func (wsm *WebServiceManager) StartWebService(config WebServiceConfig) error {
if !config.Enabled {
return nil
}
// Create handler
handler := NewWebServiceHandler(config, wsm.templateManager, wsm.logFunc)
// Create HTTP server
mux := http.NewServeMux()
mux.Handle("/", handler)
addr := fmt.Sprintf(":%d", config.Port)
server := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Store service
serviceKey := fmt.Sprintf("%s:%d", config.Name, config.Port)
wsm.services[serviceKey] = handler
log.Printf("Starting web service '%s' on port %d at path %s", config.Name, config.Port, config.Path)
// Start server
go func() {
var err error
if config.UseHTTPS {
// For HTTPS, we would need to load certificates; using HTTP placeholder
log.Printf("HTTPS requested for %s but using HTTP for now. Configure TLS certificates for production.", config.Name)
err = server.ListenAndServe()
} else {
err = server.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
log.Printf("Web service %s error: %v", config.Name, err)
}
}()
return nil
}
// StopWebService stops a web service
func (wsm *WebServiceManager) StopWebService(name string, port int) {
serviceKey := fmt.Sprintf("%s:%d", name, port)
delete(wsm.services, serviceKey)
// Note: In a production system, you'd want to properly shutdown the HTTP server
}
// GetActiveServices returns a list of active web services
func (wsm *WebServiceManager) GetActiveServices() []string {
var services []string
for key := range wsm.services {
services = append(services, key)
}
return services
}

311
app/webtemplate_manager.go Normal file
View File

@@ -0,0 +1,311 @@
package app
import (
"fmt"
"html/template"
"os"
"path/filepath"
"strings"
)
// WebTemplateManager handles HTML template management for web honeypots
type WebTemplateManager struct {
templateDir string
templates map[string]*template.Template
}
// NewWebTemplateManager creates a new template manager
func NewWebTemplateManager(configPath string) *WebTemplateManager {
templateDir := filepath.Join(filepath.Dir(configPath), "webtemplates")
// Ensure template directory exists
os.MkdirAll(templateDir, 0755)
return &WebTemplateManager{
templateDir: templateDir,
templates: make(map[string]*template.Template),
}
}
// GetTemplateDir returns the template directory path
func (wtm *WebTemplateManager) GetTemplateDir() string {
return wtm.templateDir
}
// CreateDefaultTemplate creates a default login template if it doesn't exist
func (wtm *WebTemplateManager) CreateDefaultTemplate(templateName string) error {
templatePath := filepath.Join(wtm.templateDir, templateName)
// Check if template already exists
if _, err := os.Stat(templatePath); err == nil {
return nil // Template already exists
}
defaultHTML := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - {{ .ServiceName }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #333;
font-size: 1.8rem;
margin-bottom: 0.5rem;
}
.login-header p {
color: #666;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e1e5e9;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.login-button {
width: 100%;
padding: 0.75rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease;
}
.login-button:hover {
transform: translateY(-2px);
}
.error-message {
background: #fee;
color: #c33;
padding: 0.75rem;
border-radius: 5px;
margin-bottom: 1rem;
text-align: center;
display: none;
}
.footer {
text-align: center;
margin-top: 2rem;
color: #666;
font-size: 0.8rem;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>{{ .ServiceName }}</h1>
<p>Please sign in to continue</p>
</div>
<div class="error-message" id="error-message">
Invalid username or password
</div>
<form method="POST" action="{{ .LoginPath }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="login-button">Sign In</button>
</form>
<div class="footer">
<p>&copy; 2024 {{ .ServiceName }}. All rights reserved.</p>
</div>
</div>
<script>
// Show error message if login failed
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('error') === 'invalid') {
document.getElementById('error-message').style.display = 'block';
}
// Focus on username field
document.getElementById('username').focus();
</script>
</body>
</html>`
return os.WriteFile(templatePath, []byte(defaultHTML), 0644)
}
// LoadTemplate loads and parses a template file
func (wtm *WebTemplateManager) LoadTemplate(templateName string) (*template.Template, error) {
// Check if template is already loaded
if tmpl, exists := wtm.templates[templateName]; exists {
return tmpl, nil
}
templatePath := filepath.Join(wtm.templateDir, templateName)
// Create default template if it doesn't exist
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
if err := wtm.CreateDefaultTemplate(templateName); err != nil {
return nil, fmt.Errorf("failed to create default template: %w", err)
}
}
// Parse template
tmpl, err := template.ParseFiles(templatePath)
if err != nil {
return nil, fmt.Errorf("failed to parse template %s: %w", templateName, err)
}
// Cache template
wtm.templates[templateName] = tmpl
return tmpl, nil
}
// SaveTemplate saves template content to file
func (wtm *WebTemplateManager) SaveTemplate(templateName, content string) error {
templatePath := filepath.Join(wtm.templateDir, templateName)
// Validate template by parsing it
_, err := template.New("test").Parse(content)
if err != nil {
return fmt.Errorf("invalid template syntax: %w", err)
}
// Save to file
if err := os.WriteFile(templatePath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to save template: %w", err)
}
// Remove from cache to force reload
delete(wtm.templates, templateName)
return nil
}
// GetTemplate returns template content as string
func (wtm *WebTemplateManager) GetTemplate(templateName string) (string, error) {
templatePath := filepath.Join(wtm.templateDir, templateName)
content, err := os.ReadFile(templatePath)
if err != nil {
// If template doesn't exist, create default and return it
if os.IsNotExist(err) {
if err := wtm.CreateDefaultTemplate(templateName); err != nil {
return "", fmt.Errorf("failed to create default template: %w", err)
}
content, err = os.ReadFile(templatePath)
if err != nil {
return "", fmt.Errorf("failed to read created template: %w", err)
}
} else {
return "", fmt.Errorf("failed to read template: %w", err)
}
}
return string(content), nil
}
// ListTemplates returns a list of available templates
func (wtm *WebTemplateManager) ListTemplates() ([]string, error) {
files, err := os.ReadDir(wtm.templateDir)
if err != nil {
return nil, fmt.Errorf("failed to read template directory: %w", err)
}
var templates []string
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".html") {
templates = append(templates, file.Name())
}
}
return templates, nil
}
// DeleteTemplate removes a template file
func (wtm *WebTemplateManager) DeleteTemplate(templateName string) error {
templatePath := filepath.Join(wtm.templateDir, templateName)
if err := os.Remove(templatePath); err != nil {
return fmt.Errorf("failed to delete template: %w", err)
}
// Remove from cache
delete(wtm.templates, templateName)
return nil
}
// ValidateTemplate checks if template has required fields
func (wtm *WebTemplateManager) ValidateTemplate(content string) error {
// Check for required form fields
requiredFields := []string{
`name="username"`,
`name="password"`,
`method="POST"` + ` ` + `action=`, // Split to avoid matching this comment
}
contentLower := strings.ToLower(content)
for _, field := range requiredFields {
if !strings.Contains(contentLower, strings.ToLower(field)) {
return fmt.Errorf("template missing required field: %s", field)
}
}
// Try to parse as template
_, err := template.New("validation").Parse(content)
if err != nil {
return fmt.Errorf("invalid template syntax: %w", err)
}
return nil
}

View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - {{ .ServiceName }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #333;
font-size: 1.8rem;
margin-bottom: 0.5rem;
}
.login-header p {
color: #666;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e1e5e9;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.login-button {
width: 100%;
padding: 0.75rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease;
}
.login-button:hover {
transform: translateY(-2px);
}
.error-message {
background: #fee;
color: #c33;
padding: 0.75rem;
border-radius: 5px;
margin-bottom: 1rem;
text-align: center;
display: none;
}
.footer {
text-align: center;
margin-top: 2rem;
color: #666;
font-size: 0.8rem;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>{{ .ServiceName }}</h1>
<p>Please sign in to continue</p>
</div>
<div class="error-message" id="error-message">
Invalid username or password
</div>
<form method="POST" action="{{ .LoginPath }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="login-button">Sign In</button>
</form>
<div class="footer">
<p>&copy; 2024 {{ .ServiceName }}. All rights reserved.</p>
</div>
</div>
<script>
// Show error message if login failed
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('error') === 'invalid') {
document.getElementById('error-message').style.display = 'block';
}
// Focus on username field
document.getElementById('username').focus();
</script>
</body>
</html>

34
webtemplates/test.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - </title>
<style>
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
.login-container { max-width: 400px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background: #0056b3; }
.error { color: red; margin-top: 10px; }
</style>
</head>
<body>
<div class="login-container">
<h2></h2>
<form method="POST" action="">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>