added custom web html
This commit is contained in:
+34
-5
@@ -7,15 +7,32 @@ import (
|
|||||||
"path/filepath"
|
"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
|
// Config contains runtime configuration for the honeypot
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
|
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
|
||||||
LogPath string `json:"log_path"`
|
LogPath string `json:"log_path"`
|
||||||
|
|
||||||
Web struct {
|
Web struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Bind string `json:"bind"`
|
Bind string `json:"bind"`
|
||||||
Port int `json:"port"`
|
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"`
|
} `json:"web"`
|
||||||
|
|
||||||
Services struct {
|
Services struct {
|
||||||
@@ -33,9 +50,10 @@ type Config struct {
|
|||||||
SMB bool `json:"smb"`
|
SMB bool `json:"smb"`
|
||||||
SIP bool `json:"sip"`
|
SIP bool `json:"sip"`
|
||||||
VNC bool `json:"vnc"`
|
VNC bool `json:"vnc"`
|
||||||
Generic []int `json:"generic"`
|
|
||||||
} `json:"services"`
|
} `json:"services"`
|
||||||
|
|
||||||
|
WebServices WebServicesConfig `json:"web_services"`
|
||||||
|
|
||||||
Ports struct {
|
Ports struct {
|
||||||
HTTP int `json:"http"`
|
HTTP int `json:"http"`
|
||||||
HTTPS int `json:"https"`
|
HTTPS int `json:"https"`
|
||||||
@@ -145,7 +163,6 @@ func defaultConfig() Config {
|
|||||||
c.Services.SMB = false
|
c.Services.SMB = false
|
||||||
c.Services.SIP = false
|
c.Services.SIP = false
|
||||||
c.Services.VNC = false
|
c.Services.VNC = false
|
||||||
c.Services.Generic = []int{}
|
|
||||||
|
|
||||||
// Standard ports
|
// Standard ports
|
||||||
c.Ports.HTTP = 8080
|
c.Ports.HTTP = 8080
|
||||||
@@ -174,5 +191,17 @@ func defaultConfig() Config {
|
|||||||
c.Security.BlockHighThreatIPs = false
|
c.Security.BlockHighThreatIPs = false
|
||||||
c.Security.ThreatScoreThreshold = 80
|
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
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,6 +16,8 @@ type ThreatManager struct {
|
|||||||
securityManager *SecurityManager
|
securityManager *SecurityManager
|
||||||
userAPI *UserAPI
|
userAPI *UserAPI
|
||||||
blocklistExporter *BlocklistExporter
|
blocklistExporter *BlocklistExporter
|
||||||
|
webTemplateAPI *WebTemplateAPI
|
||||||
|
webServicesAPI *WebServicesAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewThreatManager creates a new threat manager instance
|
// NewThreatManager creates a new threat manager instance
|
||||||
@@ -29,23 +32,36 @@ func NewThreatManager(dbPath string) (*ThreatManager, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize security manager
|
// Initialize security manager
|
||||||
securityManager := NewSecurityManager(authManager)
|
securityManager := NewSecurityManager(authManager)
|
||||||
|
|
||||||
// Initialize APIs
|
// Initialize APIs
|
||||||
api := NewThreatAPI(analyzer)
|
api := NewThreatAPI(analyzer)
|
||||||
userAPI := NewUserAPI(authManager, securityManager)
|
userAPI := NewUserAPI(authManager, securityManager)
|
||||||
|
|
||||||
|
// Initialize blocklist exporter
|
||||||
blocklistExporter := NewBlocklistExporter(analyzer)
|
blocklistExporter := NewBlocklistExporter(analyzer)
|
||||||
|
|
||||||
return &ThreatManager{
|
// Initialize web template API
|
||||||
analyzer: analyzer,
|
templateManager := NewWebTemplateManager("webtemplates")
|
||||||
api: api,
|
webTemplateAPI := NewWebTemplateAPI(templateManager)
|
||||||
authManager: authManager,
|
|
||||||
securityManager: securityManager,
|
// Initialize web services API
|
||||||
userAPI: userAPI,
|
configManager := NewConfigManager("config.json")
|
||||||
|
webServicesAPI := NewWebServicesAPI(configManager)
|
||||||
|
|
||||||
|
tm := &ThreatManager{
|
||||||
|
analyzer: analyzer,
|
||||||
|
api: api,
|
||||||
|
authManager: authManager,
|
||||||
|
securityManager: securityManager,
|
||||||
|
userAPI: userAPI,
|
||||||
blocklistExporter: blocklistExporter,
|
blocklistExporter: blocklistExporter,
|
||||||
}, nil
|
webTemplateAPI: webTemplateAPI,
|
||||||
|
webServicesAPI: webServicesAPI,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessHoneypotRecord processes a honeypot log record for threat analysis
|
// ProcessHoneypotRecord processes a honeypot log record for threat analysis
|
||||||
@@ -151,22 +167,30 @@ func (tm *ThreatManager) GetAPI() *ThreatAPI {
|
|||||||
func (tm *ThreatManager) GetSecurityManager() *SecurityManager {
|
func (tm *ThreatManager) GetSecurityManager() *SecurityManager {
|
||||||
return tm.securityManager
|
return tm.securityManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserAPI returns the user API instance
|
// GetUserAPI returns the user API instance
|
||||||
func (tm *ThreatManager) GetUserAPI() *UserAPI {
|
func (tm *ThreatManager) GetUserAPI() *UserAPI {
|
||||||
return tm.userAPI
|
return tm.userAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlocklistExporter returns the blocklist exporter instance
|
// GetBlocklistExporter returns the blocklist exporter
|
||||||
func (tm *ThreatManager) GetBlocklistExporter() *BlocklistExporter {
|
func (tm *ThreatManager) GetBlocklistExporter() *BlocklistExporter {
|
||||||
return tm.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
|
// GetBlockedIPs returns currently blocked IPs for firewall integration
|
||||||
func (tm *ThreatManager) GetBlockedIPs() ([]string, error) {
|
func (tm *ThreatManager) GetBlockedIPs() ([]string, error) {
|
||||||
return tm.analyzer.GetBlockedIPs()
|
return tm.analyzer.GetBlockedIPs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIPBlocked checks if an IP is currently blocked
|
// IsIPBlocked checks if an IP is currently blocked
|
||||||
func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
|
func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
|
||||||
report, err := tm.analyzer.AnalyzeIP(ip)
|
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
|
// RunPeriodicTasks starts background tasks for threat analysis
|
||||||
func (tm *ThreatManager) RunPeriodicTasks() {
|
func (tm *ThreatManager) RunPeriodicTasks(ctx context.Context) {
|
||||||
// Run threat score calculation every 5 minutes
|
// Update threat scores every 5 minutes
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for {
|
||||||
if err := tm.updateThreatScores(); err != nil {
|
select {
|
||||||
log.Printf("Failed to update threat scores: %v", err)
|
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)
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for {
|
||||||
if err := tm.evaluateAllRules(); err != nil {
|
select {
|
||||||
log.Printf("Failed to evaluate rules: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ func (sm *SecurityManager) ValidateInput() *InputValidator {
|
|||||||
return sm.validator
|
return sm.validator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateCSRFToken generates a new CSRF token
|
||||||
|
func (sm *SecurityManager) GenerateCSRFToken() (string, error) {
|
||||||
|
return sm.csrfManager.GenerateToken()
|
||||||
|
}
|
||||||
|
|
||||||
// LoginHandler handles user login
|
// LoginHandler handles user login
|
||||||
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+163
-20
@@ -22,20 +22,23 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"honeydany/app/dashboard"
|
"honeydany/app/dashboard"
|
||||||
svcs "honeydany/app/services"
|
svcs "honeydany/app/services"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App holds runtime pieces
|
// App holds runtime pieces
|
||||||
type App struct {
|
type App struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
logger *Logger
|
logger *Logger
|
||||||
threatIntel *ThreatIntel
|
threatIntel *ThreatIntel
|
||||||
threatManager *dashboard.ThreatManager
|
threatManager *dashboard.ThreatManager
|
||||||
ctx context.Context
|
webTemplateManager *WebTemplateManager
|
||||||
cancel context.CancelFunc
|
webServiceManager *WebServiceManager
|
||||||
wg sync.WaitGroup
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
// keep references to servers for graceful shutdown
|
// keep references to servers for graceful shutdown
|
||||||
httpSrvs []*http.Server
|
httpSrvs []*http.Server
|
||||||
sshSigner ssh.Signer
|
sshSigner ssh.Signer
|
||||||
@@ -64,9 +67,59 @@ func (a *App) startHTTPS(port int) {
|
|||||||
if sni := r.TLS; sni != nil && sni.ServerName != "" {
|
if sni := r.TLS; sni != nil && sni.ServerName != "" {
|
||||||
details["sni_server_name"] = 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}
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
|
||||||
a.logEvent(rec)
|
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("Server", "nginx/1.18.0 (Ubuntu)")
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
@@ -216,15 +269,53 @@ func NewApp(cfg Config) (*App, error) {
|
|||||||
tm = nil // Continue without threat manager if it fails
|
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
|
// Root context for the App used for shutdown signalling
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
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
|
// Start periodic threat analysis tasks
|
||||||
if tm != nil {
|
if tm != nil {
|
||||||
tm.RunPeriodicTasks()
|
tm.RunPeriodicTasks(a.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a, nil
|
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()))
|
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
|
// start web dashboard if enabled
|
||||||
if a.cfg.Web.Enabled {
|
if a.cfg.Web.Enabled {
|
||||||
a.wg.Add(1)
|
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()
|
<-ctx.Done()
|
||||||
a.Shutdown()
|
a.Shutdown()
|
||||||
return nil
|
return nil
|
||||||
@@ -482,9 +575,59 @@ func (a *App) startHTTP(port int) {
|
|||||||
if auth := r.Header.Get("Authorization"); auth != "" {
|
if auth := r.Header.Get("Authorization"); auth != "" {
|
||||||
details["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}
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
|
||||||
a.logEvent(rec)
|
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("Server", "Apache/2.4.41 (Ubuntu)")
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
{{ template "threat_reports_content" . }}
|
{{ template "threat_reports_content" . }}
|
||||||
{{ else if eq .PageContent "threat_rules_content" }}
|
{{ else if eq .PageContent "threat_rules_content" }}
|
||||||
{{ template "threat_rules_content" . }}
|
{{ template "threat_rules_content" . }}
|
||||||
|
{{ else if eq .PageContent "webtemplates_content" }}
|
||||||
|
{{ template "webtemplates_content" . }}
|
||||||
{{ else if eq .PageContent "users_content" }}
|
{{ else if eq .PageContent "users_content" }}
|
||||||
{{ template "users_content" . }}
|
{{ template "users_content" . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
+334
-6
@@ -127,13 +127,63 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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">
|
<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-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>
|
<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>
|
<span id="save-status" class="text-sm text-gray-400"></span>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
let currentCSRFToken = '{{ .CSRFToken }}';
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
const payload = {
|
const payload = {
|
||||||
services: {
|
services: {
|
||||||
@@ -167,17 +217,24 @@
|
|||||||
smb: parseInt(document.getElementById('port-smb').value, 10),
|
smb: parseInt(document.getElementById('port-smb').value, 10),
|
||||||
sip: parseInt(document.getElementById('port-sip').value, 10),
|
sip: parseInt(document.getElementById('port-sip').value, 10),
|
||||||
vnc: parseInt(document.getElementById('port-vnc').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 headers = { 'Content-Type': 'application/json' };
|
||||||
const csrfToken = '{{ .CSRFToken }}';
|
if (currentCSRFToken) {
|
||||||
if (csrfToken) {
|
headers['X-CSRF-Token'] = currentCSRFToken;
|
||||||
headers['X-CSRF-Token'] = csrfToken;
|
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/settings', { method: 'POST', headers: headers, body: JSON.stringify(payload) });
|
const res = await fetch('/api/settings', { method: 'POST', headers: headers, body: JSON.stringify(payload) });
|
||||||
const out = await res.json().catch(() => ({}));
|
const out = await res.json().catch(() => ({}));
|
||||||
const el = document.getElementById('save-status');
|
const el = document.getElementById('save-status');
|
||||||
if (res.ok) {
|
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.textContent = 'Saved. You may need to restart to apply port changes.';
|
||||||
el.className = 'text-sm text-green-400';
|
el.className = 'text-sm text-green-400';
|
||||||
} else {
|
} else {
|
||||||
@@ -187,12 +244,16 @@
|
|||||||
}
|
}
|
||||||
async function restartApp() {
|
async function restartApp() {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
const csrfToken = '{{ .CSRFToken }}';
|
if (currentCSRFToken) {
|
||||||
if (csrfToken) {
|
headers['X-CSRF-Token'] = currentCSRFToken;
|
||||||
headers['X-CSRF-Token'] = csrfToken;
|
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/restart', { method: 'POST', headers: headers });
|
const res = await fetch('/api/restart', { method: 'POST', headers: headers });
|
||||||
|
const out = await res.json().catch(() => ({}));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
// Update CSRF token for subsequent requests
|
||||||
|
if (out.csrf_token) {
|
||||||
|
currentCSRFToken = out.csrf_token;
|
||||||
|
}
|
||||||
document.getElementById('save-status').textContent = 'Restarting...';
|
document.getElementById('save-status').textContent = 'Restarting...';
|
||||||
setTimeout(() => location.reload(), 1200);
|
setTimeout(() => location.reload(), 1200);
|
||||||
} else {
|
} else {
|
||||||
@@ -202,5 +263,272 @@
|
|||||||
}
|
}
|
||||||
document.getElementById('btn-save').addEventListener('click', saveSettings);
|
document.getElementById('btn-save').addEventListener('click', saveSettings);
|
||||||
document.getElementById('btn-restart').addEventListener('click', restartApp);
|
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>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
+54
-3
@@ -38,6 +38,7 @@ func initTemplates() error {
|
|||||||
"templates/settings.html",
|
"templates/settings.html",
|
||||||
"templates/threat_reports.html",
|
"templates/threat_reports.html",
|
||||||
"templates/threat_rules.html",
|
"templates/threat_rules.html",
|
||||||
|
"templates/webtemplates.html",
|
||||||
"templates/users.html",
|
"templates/users.html",
|
||||||
)
|
)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
@@ -71,6 +72,12 @@ func (a *App) startWeb() {
|
|||||||
|
|
||||||
// Register blocklist export routes (public endpoints for threat intelligence sharing)
|
// Register blocklist export routes (public endpoints for threat intelligence sharing)
|
||||||
a.threatManager.GetBlocklistExporter().RegisterExportRoutes(mux, securityManager)
|
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
|
// Secure dashboard routes with authentication
|
||||||
@@ -176,6 +183,10 @@ func (a *App) startWeb() {
|
|||||||
SIP int `json:"sip"`
|
SIP int `json:"sip"`
|
||||||
VNC int `json:"vnc"`
|
VNC int `json:"vnc"`
|
||||||
} `json:"ports"`
|
} `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 {
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
@@ -213,11 +224,24 @@ func (a *App) startWeb() {
|
|||||||
a.cfg.Ports.SIP = in.Ports.SIP
|
a.cfg.Ports.SIP = in.Ports.SIP
|
||||||
a.cfg.Ports.VNC = in.Ports.VNC
|
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
|
// Persist to ./config.json
|
||||||
if b, err := json.MarshalIndent(a.cfg, "", " "); err == nil {
|
if b, err := json.MarshalIndent(a.cfg, "", " "); err == nil {
|
||||||
_ = os.WriteFile("config.json", b, 0644)
|
_ = 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
|
return
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
@@ -227,7 +251,16 @@ func (a *App) startWeb() {
|
|||||||
// Restart endpoint: triggers app restart
|
// Restart endpoint: triggers app restart
|
||||||
mux.HandleFunc("/api/restart", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/restart", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return }
|
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() }()
|
go func(){ time.Sleep(700*time.Millisecond); a.Restart() }()
|
||||||
})))
|
})))
|
||||||
mux.HandleFunc("/logs", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
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
|
var roleMiddleware func(string) func(http.HandlerFunc) http.HandlerFunc
|
||||||
if a.threatManager != nil {
|
if a.threatManager != nil {
|
||||||
roleMiddleware = a.threatManager.GetSecurityManager().RoleMiddleware
|
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) {
|
mux.HandleFunc("/users", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get current user from context
|
// Get current user from context
|
||||||
var currentUser interface{}
|
var currentUser interface{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>© 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
|
||||||
|
}
|
||||||
@@ -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>© 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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user