added custom web html
This commit is contained in:
@@ -7,15 +7,32 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// WebServiceConfig defines configuration for a custom web honeypot service
|
||||
type WebServiceConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Port int `json:"port"`
|
||||
Name string `json:"name"` // Service name (e.g., "admin-panel", "webmail")
|
||||
Path string `json:"path"` // URL path (e.g., "/login", "/admin")
|
||||
TemplateName string `json:"template_name"` // Template filename (e.g., "admin-login.html")
|
||||
UseHTTPS bool `json:"use_https"` // Whether to use HTTPS
|
||||
}
|
||||
|
||||
// WebServicesConfig holds configuration for custom web services
|
||||
type WebServicesConfig struct {
|
||||
Generic []WebServiceConfig `json:"generic"`
|
||||
}
|
||||
|
||||
// Config contains runtime configuration for the honeypot
|
||||
type Config struct {
|
||||
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
|
||||
LogPath string `json:"log_path"`
|
||||
|
||||
Web struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
HTTPTemplateName string `json:"http_template_name"` // Optional template for HTTP service
|
||||
HTTPSTemplateName string `json:"https_template_name"` // Optional template for HTTPS service
|
||||
} `json:"web"`
|
||||
|
||||
Services struct {
|
||||
@@ -33,9 +50,10 @@ type Config struct {
|
||||
SMB bool `json:"smb"`
|
||||
SIP bool `json:"sip"`
|
||||
VNC bool `json:"vnc"`
|
||||
Generic []int `json:"generic"`
|
||||
} `json:"services"`
|
||||
|
||||
WebServices WebServicesConfig `json:"web_services"`
|
||||
|
||||
Ports struct {
|
||||
HTTP int `json:"http"`
|
||||
HTTPS int `json:"https"`
|
||||
@@ -145,7 +163,6 @@ func defaultConfig() Config {
|
||||
c.Services.SMB = false
|
||||
c.Services.SIP = false
|
||||
c.Services.VNC = false
|
||||
c.Services.Generic = []int{}
|
||||
|
||||
// Standard ports
|
||||
c.Ports.HTTP = 8080
|
||||
@@ -174,5 +191,17 @@ func defaultConfig() Config {
|
||||
c.Security.BlockHighThreatIPs = false
|
||||
c.Security.ThreatScoreThreshold = 80
|
||||
|
||||
// Web services defaults - only generic services, configurable from dashboard
|
||||
c.WebServices.Generic = []WebServiceConfig{
|
||||
{
|
||||
Enabled: false,
|
||||
Port: 9001,
|
||||
Name: "admin-panel",
|
||||
Path: "/admin",
|
||||
TemplateName: "admin-login.html",
|
||||
UseHTTPS: false,
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
95
app/dashboard/config_manager.go
Normal file
95
app/dashboard/config_manager.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ConfigManager manages web services configuration
|
||||
type ConfigManager struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
// NewConfigManager creates a new config manager
|
||||
func NewConfigManager(configPath string) *ConfigManager {
|
||||
return &ConfigManager{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebServices returns the current web services configuration
|
||||
func (cm *ConfigManager) GetWebServices() []WebServiceConfig {
|
||||
// Try to read from config file
|
||||
if data, err := os.ReadFile(cm.configPath); err == nil {
|
||||
var config struct {
|
||||
WebServices struct {
|
||||
Generic []WebServiceConfig `json:"generic"`
|
||||
} `json:"web_services"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &config); err == nil {
|
||||
// If web services section exists, return it (even if empty)
|
||||
return config.WebServices.Generic
|
||||
}
|
||||
}
|
||||
|
||||
// Only return default configuration on first run (when config doesn't exist)
|
||||
// Check if this is truly the first run by looking for any config file
|
||||
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
|
||||
// First run - create default configuration
|
||||
defaultServices := []WebServiceConfig{
|
||||
{
|
||||
Enabled: false,
|
||||
Port: 9001,
|
||||
Name: "admin-panel",
|
||||
Path: "/admin",
|
||||
TemplateName: "admin-login.html",
|
||||
UseHTTPS: false,
|
||||
},
|
||||
}
|
||||
// Save the default configuration so it can be modified later
|
||||
cm.SaveWebServices(defaultServices)
|
||||
return defaultServices
|
||||
}
|
||||
|
||||
// Config file exists but web_services section is missing or corrupted
|
||||
// Return empty slice to allow full user control
|
||||
return []WebServiceConfig{}
|
||||
}
|
||||
|
||||
// SaveWebServices saves the web services configuration
|
||||
func (cm *ConfigManager) SaveWebServices(services []WebServiceConfig) error {
|
||||
// Read existing config
|
||||
var config map[string]interface{}
|
||||
if data, err := os.ReadFile(cm.configPath); err == nil {
|
||||
json.Unmarshal(data, &config)
|
||||
} else {
|
||||
config = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Update web services section
|
||||
if config["web_services"] == nil {
|
||||
config["web_services"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
webServices := config["web_services"].(map[string]interface{})
|
||||
webServices["generic"] = services
|
||||
|
||||
// Write back to file
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(cm.configPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
@@ -15,6 +16,8 @@ type ThreatManager struct {
|
||||
securityManager *SecurityManager
|
||||
userAPI *UserAPI
|
||||
blocklistExporter *BlocklistExporter
|
||||
webTemplateAPI *WebTemplateAPI
|
||||
webServicesAPI *WebServicesAPI
|
||||
}
|
||||
|
||||
// NewThreatManager creates a new threat manager instance
|
||||
@@ -29,23 +32,36 @@ func NewThreatManager(dbPath string) (*ThreatManager, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize security manager
|
||||
securityManager := NewSecurityManager(authManager)
|
||||
|
||||
// Initialize APIs
|
||||
api := NewThreatAPI(analyzer)
|
||||
userAPI := NewUserAPI(authManager, securityManager)
|
||||
|
||||
// Initialize blocklist exporter
|
||||
blocklistExporter := NewBlocklistExporter(analyzer)
|
||||
|
||||
return &ThreatManager{
|
||||
analyzer: analyzer,
|
||||
api: api,
|
||||
authManager: authManager,
|
||||
securityManager: securityManager,
|
||||
userAPI: userAPI,
|
||||
// Initialize web template API
|
||||
templateManager := NewWebTemplateManager("webtemplates")
|
||||
webTemplateAPI := NewWebTemplateAPI(templateManager)
|
||||
|
||||
// Initialize web services API
|
||||
configManager := NewConfigManager("config.json")
|
||||
webServicesAPI := NewWebServicesAPI(configManager)
|
||||
|
||||
tm := &ThreatManager{
|
||||
analyzer: analyzer,
|
||||
api: api,
|
||||
authManager: authManager,
|
||||
securityManager: securityManager,
|
||||
userAPI: userAPI,
|
||||
blocklistExporter: blocklistExporter,
|
||||
}, nil
|
||||
webTemplateAPI: webTemplateAPI,
|
||||
webServicesAPI: webServicesAPI,
|
||||
}
|
||||
|
||||
return tm, nil
|
||||
}
|
||||
|
||||
// ProcessHoneypotRecord processes a honeypot log record for threat analysis
|
||||
@@ -151,22 +167,30 @@ func (tm *ThreatManager) GetAPI() *ThreatAPI {
|
||||
func (tm *ThreatManager) GetSecurityManager() *SecurityManager {
|
||||
return tm.securityManager
|
||||
}
|
||||
|
||||
// GetUserAPI returns the user API instance
|
||||
func (tm *ThreatManager) GetUserAPI() *UserAPI {
|
||||
return tm.userAPI
|
||||
}
|
||||
|
||||
// GetBlocklistExporter returns the blocklist exporter instance
|
||||
// GetBlocklistExporter returns the blocklist exporter
|
||||
func (tm *ThreatManager) GetBlocklistExporter() *BlocklistExporter {
|
||||
return tm.blocklistExporter
|
||||
}
|
||||
|
||||
// GetWebTemplateAPI returns the web template API
|
||||
func (tm *ThreatManager) GetWebTemplateAPI() *WebTemplateAPI {
|
||||
return tm.webTemplateAPI
|
||||
}
|
||||
|
||||
// GetWebServicesAPI returns the web services API
|
||||
func (tm *ThreatManager) GetWebServicesAPI() *WebServicesAPI {
|
||||
return tm.webServicesAPI
|
||||
}
|
||||
|
||||
// GetBlockedIPs returns currently blocked IPs for firewall integration
|
||||
func (tm *ThreatManager) GetBlockedIPs() ([]string, error) {
|
||||
return tm.analyzer.GetBlockedIPs()
|
||||
}
|
||||
|
||||
// IsIPBlocked checks if an IP is currently blocked
|
||||
func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
|
||||
report, err := tm.analyzer.AnalyzeIP(ip)
|
||||
@@ -177,15 +201,23 @@ func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
|
||||
}
|
||||
|
||||
// RunPeriodicTasks starts background tasks for threat analysis
|
||||
func (tm *ThreatManager) RunPeriodicTasks() {
|
||||
// Run threat score calculation every 5 minutes
|
||||
func (tm *ThreatManager) RunPeriodicTasks(ctx context.Context) {
|
||||
// Update threat scores every 5 minutes
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := tm.updateThreatScores(); err != nil {
|
||||
log.Printf("Failed to update threat scores: %v", err)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := tm.updateThreatScores(); err != nil {
|
||||
// Only log if not a context cancellation or database closed error
|
||||
if ctx.Err() == nil && !strings.Contains(err.Error(), "database is closed") {
|
||||
log.Printf("Failed to update threat scores: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -195,9 +227,17 @@ func (tm *ThreatManager) RunPeriodicTasks() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := tm.evaluateAllRules(); err != nil {
|
||||
log.Printf("Failed to evaluate rules: %v", err)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := tm.evaluateAllRules(); err != nil {
|
||||
// Only log if not a context cancellation or database closed error
|
||||
if ctx.Err() == nil && !strings.Contains(err.Error(), "database is closed") {
|
||||
log.Printf("Failed to evaluate rules: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -179,6 +179,11 @@ func (sm *SecurityManager) ValidateInput() *InputValidator {
|
||||
return sm.validator
|
||||
}
|
||||
|
||||
// GenerateCSRFToken generates a new CSRF token
|
||||
func (sm *SecurityManager) GenerateCSRFToken() (string, error) {
|
||||
return sm.csrfManager.GenerateToken()
|
||||
}
|
||||
|
||||
// LoginHandler handles user login
|
||||
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
|
||||
105
app/dashboard/webservices_api.go
Normal file
105
app/dashboard/webservices_api.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// WebServiceConfig represents a web service configuration
|
||||
type WebServiceConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Port int `json:"port"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
TemplateName string `json:"template_name"`
|
||||
UseHTTPS bool `json:"use_https"`
|
||||
}
|
||||
|
||||
// WebServicesAPI handles web services configuration API endpoints
|
||||
type WebServicesAPI struct {
|
||||
configManager interface {
|
||||
GetWebServices() []WebServiceConfig
|
||||
SaveWebServices([]WebServiceConfig) error
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebServicesAPI creates a new web services API instance
|
||||
func NewWebServicesAPI(configManager interface {
|
||||
GetWebServices() []WebServiceConfig
|
||||
SaveWebServices([]WebServiceConfig) error
|
||||
}) *WebServicesAPI {
|
||||
return &WebServicesAPI{configManager: configManager}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers the web services API routes
|
||||
func (wsa *WebServicesAPI) RegisterRoutes(mux *http.ServeMux, sm *SecurityManager) {
|
||||
// Require admin auth for both GET and POST.
|
||||
mux.HandleFunc("/api/webservices", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wsa.handleWebServices)))
|
||||
}
|
||||
|
||||
// handleWebServices handles GET (list) and POST (save) operations for web services
|
||||
func (wsa *WebServicesAPI) handleWebServices(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
wsa.handleGetWebServices(w, r)
|
||||
case http.MethodPost:
|
||||
wsa.handleSaveWebServices(w, r)
|
||||
default:
|
||||
wsa.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetWebServices returns the current web services configuration
|
||||
func (wsa *WebServicesAPI) handleGetWebServices(w http.ResponseWriter, r *http.Request) {
|
||||
services := wsa.configManager.GetWebServices()
|
||||
resp := map[string]interface{}{
|
||||
"services": services,
|
||||
"count": len(services),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// handleSaveWebServices saves the web services configuration
|
||||
func (wsa *WebServicesAPI) handleSaveWebServices(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Services []WebServiceConfig `json:"services"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
wsa.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate services
|
||||
for i, s := range req.Services {
|
||||
if s.Name == "" {
|
||||
wsa.sendJSONError(w, "Service name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if s.Port <= 0 || s.Port > 65535 {
|
||||
wsa.sendJSONError(w, "Invalid port number", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if s.Path == "" {
|
||||
req.Services[i].Path = "/"
|
||||
}
|
||||
if s.Path[0] != '/' {
|
||||
req.Services[i].Path = "/" + s.Path
|
||||
}
|
||||
}
|
||||
|
||||
if err := wsa.configManager.SaveWebServices(req.Services); err != nil {
|
||||
wsa.sendJSONError(w, "Failed to save web services", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Web services saved successfully"})
|
||||
}
|
||||
|
||||
// sendJSONError sends a JSON error response
|
||||
func (wsa *WebServicesAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
228
app/dashboard/webtemplate_api.go
Normal file
228
app/dashboard/webtemplate_api.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WebTemplateAPI exposes endpoints to manage web templates
|
||||
type WebTemplateAPI struct {
|
||||
templateManager interface {
|
||||
GetTemplate(name string) (string, error)
|
||||
SaveTemplate(name, content string) error
|
||||
ListTemplates() ([]string, error)
|
||||
DeleteTemplate(name string) error
|
||||
ValidateTemplate(content string) error
|
||||
CreateDefaultTemplate(name string) error
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebTemplateAPI constructs the API wrapper
|
||||
func NewWebTemplateAPI(tm interface {
|
||||
GetTemplate(name string) (string, error)
|
||||
SaveTemplate(name, content string) error
|
||||
ListTemplates() ([]string, error)
|
||||
DeleteTemplate(name string) error
|
||||
ValidateTemplate(content string) error
|
||||
CreateDefaultTemplate(name string) error
|
||||
}) *WebTemplateAPI {
|
||||
return &WebTemplateAPI{templateManager: tm}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all endpoints (guarded by security middleware from caller)
|
||||
func (wta *WebTemplateAPI) RegisterRoutes(mux *http.ServeMux, sm *SecurityManager) {
|
||||
mux.HandleFunc("/api/webtemplates", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplates)))
|
||||
mux.HandleFunc("/api/webtemplates/", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplate)))
|
||||
mux.HandleFunc("/api/webtemplates/validate", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleValidateTemplate)))
|
||||
}
|
||||
|
||||
// handleTemplates supports list and create
|
||||
func (wta *WebTemplateAPI) handleTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
wta.handleListTemplates(w, r)
|
||||
case http.MethodPost:
|
||||
wta.handleCreateTemplate(w, r)
|
||||
default:
|
||||
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTemplate supports get/update/delete for a specific template name in the path
|
||||
func (wta *WebTemplateAPI) handleTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/api/webtemplates/")
|
||||
if name == "" || name == "/" {
|
||||
wta.sendJSONError(w, "Template name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Normalize (strip leading slash, append .html if missing)
|
||||
name = strings.TrimPrefix(strings.TrimSpace(name), "/")
|
||||
if name != "" && !strings.HasSuffix(strings.ToLower(name), ".html") {
|
||||
name += ".html"
|
||||
}
|
||||
if !wta.isValidTemplateName(name) {
|
||||
wta.sendJSONError(w, "Invalid template name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
wta.handleGetTemplate(w, r, name)
|
||||
case http.MethodPut:
|
||||
wta.handleUpdateTemplate(w, r, name)
|
||||
case http.MethodDelete:
|
||||
wta.handleDeleteTemplate(w, r, name)
|
||||
default:
|
||||
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
func (wta *WebTemplateAPI) handleListTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
names, err := wta.templateManager.ListTemplates()
|
||||
if err != nil {
|
||||
wta.sendJSONError(w, "Failed to list templates", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"templates": names,
|
||||
"count": len(names),
|
||||
})
|
||||
}
|
||||
|
||||
// Get
|
||||
func (wta *WebTemplateAPI) handleGetTemplate(w http.ResponseWriter, r *http.Request, name string) {
|
||||
content, err := wta.templateManager.GetTemplate(name)
|
||||
if err != nil {
|
||||
wta.sendJSONError(w, "Template not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"name": name, "content": content})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (wta *WebTemplateAPI) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name != "" && !strings.HasSuffix(strings.ToLower(req.Name), ".html") {
|
||||
req.Name += ".html"
|
||||
}
|
||||
if !wta.isValidTemplateName(req.Name) {
|
||||
wta.sendJSONError(w, fmt.Sprintf("Invalid template name: %v", req.Name), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Content) == "" {
|
||||
if err := wta.templateManager.CreateDefaultTemplate(req.Name); err != nil {
|
||||
wta.sendJSONError(w, fmt.Sprintf("Failed to create default template: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
|
||||
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := wta.templateManager.SaveTemplate(req.Name, req.Content); err != nil {
|
||||
wta.sendJSONError(w, fmt.Sprintf("Failed to save template: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template created successfully", "name": req.Name})
|
||||
}
|
||||
|
||||
// Update
|
||||
func (wta *WebTemplateAPI) handleUpdateTemplate(w http.ResponseWriter, r *http.Request, name string) {
|
||||
var req struct{ Content string `json:"content"` }
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
|
||||
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := wta.templateManager.SaveTemplate(name, req.Content); err != nil {
|
||||
wta.sendJSONError(w, "Failed to update template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template updated successfully", "name": name})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (wta *WebTemplateAPI) handleDeleteTemplate(w http.ResponseWriter, r *http.Request, name string) {
|
||||
if err := wta.templateManager.DeleteTemplate(name); err != nil {
|
||||
wta.sendJSONError(w, "Failed to delete template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template deleted successfully", "name": name})
|
||||
}
|
||||
|
||||
// Validate (content only)
|
||||
func (wta *WebTemplateAPI) handleValidateTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct{ Content string `json:"content"` }
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"valid": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"valid": true})
|
||||
}
|
||||
|
||||
// Validation helper for names
|
||||
func (wta *WebTemplateAPI) isValidTemplateName(name string) bool {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".html") {
|
||||
return false
|
||||
}
|
||||
// Disallow path separators and traversal
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
// Must be a base filename
|
||||
if name != filepath.Base(name) {
|
||||
return false
|
||||
}
|
||||
if len(name) > 100 {
|
||||
return false
|
||||
}
|
||||
// Only safe characters
|
||||
for i := 0; i < len(name); i++ {
|
||||
c := name[i]
|
||||
if !(c == '-' || c == '_' || c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wta *WebTemplateAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
220
app/dashboard/webtemplate_manager.go
Normal file
220
app/dashboard/webtemplate_manager.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WebTemplateManager manages HTML templates for web honeypot services
|
||||
type WebTemplateManager struct {
|
||||
templateDir string
|
||||
}
|
||||
|
||||
// NewWebTemplateManager creates a new web template manager
|
||||
func NewWebTemplateManager(templateDir string) *WebTemplateManager {
|
||||
// Ensure template directory exists
|
||||
if err := os.MkdirAll(templateDir, 0755); err != nil {
|
||||
// Log error but continue - directory will be created when first template is saved
|
||||
}
|
||||
|
||||
return &WebTemplateManager{
|
||||
templateDir: templateDir,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplate retrieves a template by name
|
||||
func (wtm *WebTemplateManager) GetTemplate(templateName string) (string, error) {
|
||||
if !wtm.isValidTemplateName(templateName) {
|
||||
return "", fmt.Errorf("invalid template name: %s", templateName)
|
||||
}
|
||||
|
||||
templatePath := filepath.Join(wtm.templateDir, templateName)
|
||||
content, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template %s: %w", templateName, err)
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// SaveTemplate saves a template with the given name and content
|
||||
func (wtm *WebTemplateManager) SaveTemplate(templateName, content string) error {
|
||||
if !wtm.isValidTemplateName(templateName) {
|
||||
return fmt.Errorf("invalid template name: %s", templateName)
|
||||
}
|
||||
|
||||
// Ensure template directory exists
|
||||
if err := os.MkdirAll(wtm.templateDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create template directory: %w", err)
|
||||
}
|
||||
|
||||
templatePath := filepath.Join(wtm.templateDir, templateName)
|
||||
if err := os.WriteFile(templatePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to save template %s: %w", templateName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTemplates returns a list of available template names
|
||||
func (wtm *WebTemplateManager) ListTemplates() ([]string, error) {
|
||||
if _, err := os.Stat(wtm.templateDir); os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(wtm.templateDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template directory: %w", err)
|
||||
}
|
||||
|
||||
var templates []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
|
||||
templates = append(templates, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
// DeleteTemplate removes a template
|
||||
func (wtm *WebTemplateManager) DeleteTemplate(templateName string) error {
|
||||
if !wtm.isValidTemplateName(templateName) {
|
||||
return fmt.Errorf("invalid template name: %s", templateName)
|
||||
}
|
||||
|
||||
templatePath := filepath.Join(wtm.templateDir, templateName)
|
||||
if err := os.Remove(templatePath); err != nil {
|
||||
return fmt.Errorf("failed to delete template %s: %w", templateName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTemplate validates template content for basic HTML structure
|
||||
func (wtm *WebTemplateManager) ValidateTemplate(content string) error {
|
||||
// Basic validation - check if it's valid HTML template
|
||||
_, err := template.New("test").Parse(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid template syntax: %w", err)
|
||||
}
|
||||
|
||||
// Additional validation - ensure it contains basic HTML structure
|
||||
content = strings.ToLower(content)
|
||||
if !strings.Contains(content, "<html") {
|
||||
return fmt.Errorf("template must contain <html> tag")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "<body") {
|
||||
return fmt.Errorf("template must contain <body> tag")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDefaultTemplate creates a default template with the given name
|
||||
func (wtm *WebTemplateManager) CreateDefaultTemplate(templateName string) error {
|
||||
if !wtm.isValidTemplateName(templateName) {
|
||||
return fmt.Errorf("invalid template name: %s", templateName)
|
||||
}
|
||||
|
||||
defaultContent := `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - {{ .ServiceName }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>{{ .ServiceName }}</h2>
|
||||
<form method="POST" action="{{ .LoginPath }}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return wtm.SaveTemplate(templateName, defaultContent)
|
||||
}
|
||||
|
||||
// isValidTemplateName validates template name for security
|
||||
func (wtm *WebTemplateManager) isValidTemplateName(name string) bool {
|
||||
name = strings.TrimSpace(name)
|
||||
// Must end with .html
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".html") {
|
||||
return false
|
||||
}
|
||||
// Disallow path separators and traversal, but allow single dots
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
// Must not be empty or too long
|
||||
if len(name) == 0 || len(name) > 100 {
|
||||
return false
|
||||
}
|
||||
// Must be a valid filename
|
||||
if name != filepath.Base(name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
183
app/services.go
183
app/services.go
@@ -22,20 +22,23 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"honeydany/app/dashboard"
|
||||
svcs "honeydany/app/services"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// App holds runtime pieces
|
||||
type App struct {
|
||||
cfg Config
|
||||
logger *Logger
|
||||
threatIntel *ThreatIntel
|
||||
threatManager *dashboard.ThreatManager
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
cfg Config
|
||||
logger *Logger
|
||||
threatIntel *ThreatIntel
|
||||
threatManager *dashboard.ThreatManager
|
||||
webTemplateManager *WebTemplateManager
|
||||
webServiceManager *WebServiceManager
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
// keep references to servers for graceful shutdown
|
||||
httpSrvs []*http.Server
|
||||
sshSigner ssh.Signer
|
||||
@@ -64,9 +67,59 @@ func (a *App) startHTTPS(port int) {
|
||||
if sni := r.TLS; sni != nil && sni.ServerName != "" {
|
||||
details["sni_server_name"] = sni.ServerName
|
||||
}
|
||||
|
||||
// Handle POST requests (login attempts)
|
||||
if r.Method == http.MethodPost {
|
||||
if err := r.ParseForm(); err == nil {
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
if username != "" || password != "" {
|
||||
details["login_attempt"] = "true"
|
||||
details["username"] = username
|
||||
details["password"] = password
|
||||
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
|
||||
|
||||
// Log the login attempt
|
||||
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
|
||||
a.logEvent(rec)
|
||||
|
||||
// Redirect back with error (honeypot always rejects)
|
||||
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
|
||||
a.logEvent(rec)
|
||||
|
||||
// Check if template is configured
|
||||
if a.cfg.Web.HTTPSTemplateName != "" && a.webTemplateManager != nil {
|
||||
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPSTemplateName)
|
||||
if err == nil {
|
||||
// Render template with context data compatible with admin-login.html
|
||||
data := map[string]interface{}{
|
||||
"ServiceName": "HTTPS Service",
|
||||
"LoginPath": r.URL.Path,
|
||||
"UseHTTPS": true,
|
||||
}
|
||||
|
||||
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("HTTPS template execution error: %v", err)
|
||||
_, _ = w.Write([]byte("Welcome (TLS)\n"))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Printf("HTTPS template load error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Default response if no template or template failed
|
||||
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(200)
|
||||
@@ -216,15 +269,53 @@ func NewApp(cfg Config) (*App, error) {
|
||||
tm = nil // Continue without threat manager if it fails
|
||||
}
|
||||
|
||||
// Initialize web template manager
|
||||
configPath := "config.json" // Default path, should be passed from main
|
||||
if LastConfigPath != "" {
|
||||
configPath = LastConfigPath
|
||||
}
|
||||
webTemplateManager := NewWebTemplateManager(configPath)
|
||||
|
||||
// Initialize web service manager
|
||||
webServiceManager := NewWebServiceManager(webTemplateManager, func(record Record) {
|
||||
// Use the same logging function as other services
|
||||
if l != nil {
|
||||
_ = l.Log(record)
|
||||
}
|
||||
// Also process with threat intelligence
|
||||
if ti != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
|
||||
ti.RecordActivity(record)
|
||||
}
|
||||
// Process with threat manager
|
||||
if tm != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
|
||||
details := make(map[string]interface{})
|
||||
for k, v := range record.Details {
|
||||
details[k] = v
|
||||
}
|
||||
tm.ProcessHoneypotRecord(record.Timestamp, record.RemoteAddr, record.RemotePort, record.Service, details, record.RawPayload)
|
||||
}
|
||||
})
|
||||
|
||||
// Root context for the App used for shutdown signalling
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a := &App{cfg: cfg, logger: l, threatIntel: ti, threatManager: tm, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1)}
|
||||
|
||||
a := &App{
|
||||
cfg: cfg,
|
||||
logger: l,
|
||||
threatIntel: ti,
|
||||
threatManager: tm,
|
||||
webTemplateManager: webTemplateManager,
|
||||
webServiceManager: webServiceManager,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
conns: make(map[net.Conn]struct{}),
|
||||
restartCh: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
// Start periodic threat analysis tasks
|
||||
if tm != nil {
|
||||
tm.RunPeriodicTasks()
|
||||
tm.RunPeriodicTasks(a.ctx)
|
||||
}
|
||||
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -322,14 +413,7 @@ func (a *App) Run(ctx context.Context) error {
|
||||
a.startTCPService("vnc", a.cfg.Ports.VNC, svcs.NewVNCHandler(a.svcLogger()))
|
||||
}()
|
||||
}
|
||||
for _, p := range a.cfg.Services.Generic {
|
||||
port := p
|
||||
a.wg.Add(1)
|
||||
go func() {
|
||||
defer a.wg.Done()
|
||||
a.startTCPService(fmt.Sprintf("generic-%d", port), port, svcs.NewGenericEchoHandler(a.svcLogger()))
|
||||
}()
|
||||
}
|
||||
|
||||
// start web dashboard if enabled
|
||||
if a.cfg.Web.Enabled {
|
||||
a.wg.Add(1)
|
||||
@@ -339,6 +423,15 @@ func (a *App) Run(ctx context.Context) error {
|
||||
}()
|
||||
}
|
||||
|
||||
// Start custom web services (only generic services)
|
||||
for _, genericService := range a.cfg.WebServices.Generic {
|
||||
if genericService.Enabled {
|
||||
if err := a.webServiceManager.StartWebService(genericService); err != nil {
|
||||
log.Printf("Failed to start web service %s: %v", genericService.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
a.Shutdown()
|
||||
return nil
|
||||
@@ -482,9 +575,59 @@ func (a *App) startHTTP(port int) {
|
||||
if auth := r.Header.Get("Authorization"); auth != "" {
|
||||
details["authorization"] = auth
|
||||
}
|
||||
|
||||
// Handle POST requests (login attempts)
|
||||
if r.Method == http.MethodPost {
|
||||
if err := r.ParseForm(); err == nil {
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
if username != "" || password != "" {
|
||||
details["login_attempt"] = "true"
|
||||
details["username"] = username
|
||||
details["password"] = password
|
||||
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
|
||||
|
||||
// Log the login attempt
|
||||
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
|
||||
a.logEvent(rec)
|
||||
|
||||
// Redirect back with error (honeypot always rejects)
|
||||
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
|
||||
a.logEvent(rec)
|
||||
|
||||
// Check if template is configured
|
||||
if a.cfg.Web.HTTPTemplateName != "" && a.webTemplateManager != nil {
|
||||
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPTemplateName)
|
||||
if err == nil {
|
||||
// Render template with context data compatible with admin-login.html
|
||||
data := map[string]interface{}{
|
||||
"ServiceName": "HTTP Service",
|
||||
"LoginPath": r.URL.Path,
|
||||
"UseHTTPS": false,
|
||||
}
|
||||
|
||||
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("HTTP template execution error: %v", err)
|
||||
_, _ = w.Write([]byte("Welcome\n"))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Printf("HTTP template load error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Default response if no template or template failed
|
||||
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(200)
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
{{ template "threat_reports_content" . }}
|
||||
{{ else if eq .PageContent "threat_rules_content" }}
|
||||
{{ template "threat_rules_content" . }}
|
||||
{{ else if eq .PageContent "webtemplates_content" }}
|
||||
{{ template "webtemplates_content" . }}
|
||||
{{ else if eq .PageContent "users_content" }}
|
||||
{{ template "users_content" . }}
|
||||
{{ end }}
|
||||
|
||||
@@ -127,13 +127,63 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTTP/HTTPS Templates Section -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">HTTP/HTTPS Templates</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label class="block">
|
||||
<span class="text-gray-300">HTTP Template</span>
|
||||
<select id="http-template" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" data-current-value="{{ .Cfg.Web.HTTPTemplateName }}">
|
||||
<option value="">None (Default Welcome)</option>
|
||||
<!-- Templates will be loaded here -->
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-gray-300">HTTPS Template</span>
|
||||
<select id="https-template" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" data-current-value="{{ .Cfg.Web.HTTPSTemplateName }}">
|
||||
<option value="">None (Default Welcome)</option>
|
||||
<!-- Templates will be loaded here -->
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Web Services Section -->
|
||||
<div class="mt-6 bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-white">Web Services</h2>
|
||||
<button id="btn-add-webservice" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">
|
||||
Add Service
|
||||
</button>
|
||||
</div>
|
||||
<div id="webservices-list" class="space-y-4">
|
||||
<!-- Web services will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Web Templates Section -->
|
||||
<div class="mt-6 bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-white">Web Templates</h2>
|
||||
<button id="btn-manage-templates" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm">
|
||||
Manage Templates
|
||||
</button>
|
||||
</div>
|
||||
<div id="templates-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<!-- Templates will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center gap-3">
|
||||
<button id="btn-save" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">Save Settings</button>
|
||||
<button id="btn-restart" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">Restart App</button>
|
||||
<span id="save-status" class="text-sm text-gray-400"></span>
|
||||
</div>
|
||||
<script>
|
||||
let currentCSRFToken = '{{ .CSRFToken }}';
|
||||
|
||||
async function saveSettings() {
|
||||
const payload = {
|
||||
services: {
|
||||
@@ -167,17 +217,24 @@
|
||||
smb: parseInt(document.getElementById('port-smb').value, 10),
|
||||
sip: parseInt(document.getElementById('port-sip').value, 10),
|
||||
vnc: parseInt(document.getElementById('port-vnc').value, 10),
|
||||
},
|
||||
web: {
|
||||
http_template_name: document.getElementById('http-template').value,
|
||||
https_template_name: document.getElementById('https-template').value,
|
||||
}
|
||||
};
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const csrfToken = '{{ .CSRFToken }}';
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
if (currentCSRFToken) {
|
||||
headers['X-CSRF-Token'] = currentCSRFToken;
|
||||
}
|
||||
const res = await fetch('/api/settings', { method: 'POST', headers: headers, body: JSON.stringify(payload) });
|
||||
const out = await res.json().catch(() => ({}));
|
||||
const el = document.getElementById('save-status');
|
||||
if (res.ok) {
|
||||
// Update CSRF token for subsequent requests
|
||||
if (out.csrf_token) {
|
||||
currentCSRFToken = out.csrf_token;
|
||||
}
|
||||
el.textContent = 'Saved. You may need to restart to apply port changes.';
|
||||
el.className = 'text-sm text-green-400';
|
||||
} else {
|
||||
@@ -187,12 +244,16 @@
|
||||
}
|
||||
async function restartApp() {
|
||||
const headers = {};
|
||||
const csrfToken = '{{ .CSRFToken }}';
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
if (currentCSRFToken) {
|
||||
headers['X-CSRF-Token'] = currentCSRFToken;
|
||||
}
|
||||
const res = await fetch('/api/restart', { method: 'POST', headers: headers });
|
||||
const out = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
// Update CSRF token for subsequent requests
|
||||
if (out.csrf_token) {
|
||||
currentCSRFToken = out.csrf_token;
|
||||
}
|
||||
document.getElementById('save-status').textContent = 'Restarting...';
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} else {
|
||||
@@ -202,5 +263,272 @@
|
||||
}
|
||||
document.getElementById('btn-save').addEventListener('click', saveSettings);
|
||||
document.getElementById('btn-restart').addEventListener('click', restartApp);
|
||||
|
||||
// Web Templates Management
|
||||
document.getElementById('btn-manage-templates').addEventListener('click', () => {
|
||||
openTemplateManager();
|
||||
});
|
||||
|
||||
// Web Services Management
|
||||
document.getElementById('btn-add-webservice').addEventListener('click', () => {
|
||||
addWebService();
|
||||
});
|
||||
|
||||
// Load initial data
|
||||
loadWebServices();
|
||||
loadTemplates();
|
||||
|
||||
async function loadWebServices() {
|
||||
try {
|
||||
console.log('Loading web services from server...');
|
||||
const response = await fetch('/api/webservices');
|
||||
const data = await response.json();
|
||||
console.log('Loaded web services data:', data);
|
||||
webServices = data.services || [];
|
||||
console.log('webServices array after load:', webServices);
|
||||
renderWebServices(webServices);
|
||||
} catch (error) {
|
||||
console.error('Failed to load web services:', error);
|
||||
webServices = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const response = await fetch('/api/webtemplates');
|
||||
const data = await response.json();
|
||||
renderTemplates(data.templates || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load templates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWebServices(services) {
|
||||
const container = document.getElementById('webservices-list');
|
||||
if (services.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-4">No web services configured</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the global webServices array to keep it in sync
|
||||
webServices = [...services];
|
||||
|
||||
container.innerHTML = services.map((service, index) => `
|
||||
<div class="bg-gray-900 border border-gray-600 rounded p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 items-center">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Name</label>
|
||||
<input type="text" value="${service.name}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
|
||||
onchange="updateWebService(${index}, 'name', this.value)">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Port</label>
|
||||
<input type="number" value="${service.port}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
|
||||
onchange="updateWebService(${index}, 'port', parseInt(this.value))">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Path</label>
|
||||
<input type="text" value="${service.path}" class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
|
||||
onchange="updateWebService(${index}, 'path', this.value)">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Template</label>
|
||||
<select class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-gray-100 text-sm"
|
||||
onchange="updateWebService(${index}, 'template_name', this.value)">
|
||||
<option value="">None (200 OK)</option>
|
||||
${getTemplateOptions(service.template_name)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" ${service.enabled ? 'checked' : ''} class="mr-1"
|
||||
onchange="updateWebService(${index}, 'enabled', this.checked)">
|
||||
<span class="text-sm text-gray-300">Enabled</span>
|
||||
</label>
|
||||
<button onclick="removeWebService(${index})" class="text-red-400 hover:text-red-300 text-sm">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function getTemplateOptions(selectedTemplate) {
|
||||
if (!window.availableTemplates || window.availableTemplates.length === 0) {
|
||||
return '<option value="" disabled>Loading templates...</option>';
|
||||
}
|
||||
return window.availableTemplates.map(t =>
|
||||
`<option value="${t}" ${selectedTemplate === t ? 'selected' : ''}>${t}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderTemplates(templates) {
|
||||
window.availableTemplates = templates;
|
||||
const container = document.getElementById('templates-list');
|
||||
if (templates.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-4 col-span-full">No templates found</p>';
|
||||
} else {
|
||||
container.innerHTML = templates.map(template => `
|
||||
<div class="bg-gray-900 border border-gray-600 rounded p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-100 text-sm font-medium">${template}</span>
|
||||
<button onclick="editTemplate('${template}')" class="text-blue-400 hover:text-blue-300 text-xs">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update HTTP/HTTPS template dropdowns
|
||||
updateTemplateDropdowns(templates);
|
||||
|
||||
// Re-render web services to update their template dropdowns
|
||||
if (webServices && webServices.length > 0) {
|
||||
renderWebServices(webServices);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTemplateDropdowns(templates) {
|
||||
const httpSelect = document.getElementById('http-template');
|
||||
const httpsSelect = document.getElementById('https-template');
|
||||
|
||||
// Get current values from data attributes or current selection
|
||||
const currentHttpTemplate = httpSelect.dataset.currentValue || httpSelect.value;
|
||||
const currentHttpsTemplate = httpsSelect.dataset.currentValue || httpsSelect.value;
|
||||
|
||||
// Clear and repopulate options
|
||||
[httpSelect, httpsSelect].forEach(select => {
|
||||
// Keep the "None" option
|
||||
select.innerHTML = '<option value="">None (Default Welcome)</option>';
|
||||
templates.forEach(template => {
|
||||
const option = document.createElement('option');
|
||||
option.value = template;
|
||||
option.textContent = template;
|
||||
select.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
// Set current values
|
||||
httpSelect.value = currentHttpTemplate;
|
||||
httpsSelect.value = currentHttpsTemplate;
|
||||
|
||||
// Clear data attributes after first use
|
||||
delete httpSelect.dataset.currentValue;
|
||||
delete httpsSelect.dataset.currentValue;
|
||||
}
|
||||
|
||||
let webServices = [];
|
||||
|
||||
function addWebService() {
|
||||
const newService = {
|
||||
enabled: false,
|
||||
port: 9000 + webServices.length,
|
||||
name: `service-${webServices.length + 1}`,
|
||||
path: '/login',
|
||||
template_name: '',
|
||||
use_https: false
|
||||
};
|
||||
webServices.push(newService);
|
||||
renderWebServices(webServices);
|
||||
}
|
||||
|
||||
async function updateWebService(index, field, value) {
|
||||
if (webServices[index]) {
|
||||
webServices[index][field] = value;
|
||||
// Auto-save web services when changed
|
||||
await saveWebServices();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWebServices() {
|
||||
try {
|
||||
console.log('Saving web services:', webServices);
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (currentCSRFToken) {
|
||||
headers['X-CSRF-Token'] = currentCSRFToken;
|
||||
}
|
||||
|
||||
const payload = { services: webServices };
|
||||
console.log('Payload being sent:', payload);
|
||||
|
||||
const response = await fetch('/api/webservices', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Server response:', result);
|
||||
|
||||
if (response.ok) {
|
||||
// Update CSRF token if provided
|
||||
if (result.csrf_token) {
|
||||
currentCSRFToken = result.csrf_token;
|
||||
}
|
||||
console.log('Web services saved successfully');
|
||||
// Restart app so changes (like template swap) take effect on running web services
|
||||
try {
|
||||
const hdrs = {};
|
||||
if (currentCSRFToken) hdrs['X-CSRF-Token'] = currentCSRFToken;
|
||||
const rres = await fetch('/api/restart', { method: 'POST', headers: hdrs });
|
||||
if (rres.ok) {
|
||||
console.log('Restart triggered');
|
||||
} else {
|
||||
console.warn('Restart request failed');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error triggering restart', e);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to save web services:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving web services:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeWebService(index) {
|
||||
webServices.splice(index, 1);
|
||||
console.log('After removal, webServices:', webServices);
|
||||
|
||||
// Handle empty services array
|
||||
if (webServices.length === 0) {
|
||||
document.getElementById('webservices-list').innerHTML = '<p class="text-gray-400 text-center py-4">No web services configured</p>';
|
||||
} else {
|
||||
renderWebServices(webServices);
|
||||
}
|
||||
|
||||
// Auto-save after removal
|
||||
await saveWebServices();
|
||||
}
|
||||
|
||||
function openTemplateManager() {
|
||||
// Open template manager modal (we'll embed the template manager here)
|
||||
showTemplateManagerModal();
|
||||
}
|
||||
|
||||
function editTemplate(templateName) {
|
||||
showTemplateManagerModal(templateName);
|
||||
}
|
||||
|
||||
function showTemplateManagerModal(templateName = null) {
|
||||
// Create and show the template manager modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
||||
modal.innerHTML = `
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 w-full max-w-6xl h-[90vh] flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center shrink-0">
|
||||
<h3 class="text-lg font-semibold text-gray-100">Template Manager</h3>
|
||||
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 p-4 overflow-hidden">
|
||||
<iframe src="/webtemplates" class="w-full h-full min-h-0 border-0 rounded"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
382
app/templates/webtemplates.html
Normal file
382
app/templates/webtemplates.html
Normal file
@@ -0,0 +1,382 @@
|
||||
{{ define "webtemplates_content" }}
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-100">Web Templates</h1>
|
||||
<p class="text-gray-400 mt-1">Manage HTML templates for web honeypot services</p>
|
||||
</div>
|
||||
<button id="btn-new-template" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium">
|
||||
New Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Template List -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700">
|
||||
<div class="p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Available Templates</h2>
|
||||
<div id="template-list" class="space-y-3">
|
||||
<!-- Templates will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Editor Modal -->
|
||||
<div id="template-editor-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 w-full max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<div class="p-6 border-b border-gray-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 id="editor-title" class="text-lg font-semibold text-gray-100">Edit Template</h3>
|
||||
<button id="btn-close-editor" class="text-gray-400 hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
|
||||
<!-- Editor -->
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<label class="text-sm font-medium text-gray-300">Template Name</label>
|
||||
<button id="btn-validate" class="text-sm bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded">
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
<input id="template-name" type="text" placeholder="template-name.html"
|
||||
class="mb-3 bg-gray-900 border border-gray-600 rounded px-3 py-2 text-gray-100 text-sm">
|
||||
|
||||
<label class="text-sm font-medium text-gray-300 mb-2">HTML Content</label>
|
||||
<textarea id="template-content"
|
||||
class="flex-1 bg-gray-900 border border-gray-600 rounded px-3 py-2 text-gray-100 font-mono text-sm resize-none min-h-96"
|
||||
placeholder="Enter your HTML template here..."
|
||||
spellcheck="false"
|
||||
style="tab-size: 2; white-space: pre; overflow-wrap: normal; overflow-x: auto;"></textarea>
|
||||
|
||||
<div id="validation-result" class="mt-2 text-sm hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-300 mb-2">Preview</label>
|
||||
<div class="flex-1 border border-gray-600 rounded overflow-hidden">
|
||||
<iframe id="template-preview" class="w-full h-full bg-white"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-700 flex justify-between">
|
||||
<div class="space-x-2">
|
||||
<button id="btn-preview" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
|
||||
Update Preview
|
||||
</button>
|
||||
<button id="btn-create-default" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded">
|
||||
Load Default Template
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button id="btn-cancel-edit" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="btn-save-template" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
|
||||
Save Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="status-message" class="hidden fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentTemplate = null;
|
||||
let templates = [];
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTemplates();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('btn-new-template').addEventListener('click', () => openEditor());
|
||||
document.getElementById('btn-close-editor').addEventListener('click', closeEditor);
|
||||
document.getElementById('btn-cancel-edit').addEventListener('click', closeEditor);
|
||||
document.getElementById('btn-save-template').addEventListener('click', saveTemplate);
|
||||
document.getElementById('btn-validate').addEventListener('click', validateTemplate);
|
||||
document.getElementById('btn-preview').addEventListener('click', updatePreview);
|
||||
document.getElementById('btn-create-default').addEventListener('click', loadDefaultTemplate);
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const response = await fetch('/api/webtemplates');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
templates = data.templates || [];
|
||||
renderTemplateList();
|
||||
} else {
|
||||
showStatus('Failed to load templates: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus('Error loading templates: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderTemplateList() {
|
||||
const container = document.getElementById('template-list');
|
||||
|
||||
if (templates.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-8">No templates found. Create your first template!</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = templates.map(template => `
|
||||
<div class="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-600">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-100 font-medium">${template}</span>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button onclick="editTemplate('${template}')" class="text-blue-400 hover:text-blue-300 text-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button onclick="deleteTemplate('${template}')" class="text-red-400 hover:text-red-300 text-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openEditor(templateName = null) {
|
||||
currentTemplate = templateName;
|
||||
|
||||
if (templateName) {
|
||||
document.getElementById('editor-title').textContent = 'Edit Template';
|
||||
document.getElementById('template-name').value = templateName;
|
||||
document.getElementById('template-name').disabled = true;
|
||||
loadTemplateContent(templateName);
|
||||
} else {
|
||||
document.getElementById('editor-title').textContent = 'New Template';
|
||||
document.getElementById('template-name').value = '';
|
||||
document.getElementById('template-name').disabled = false;
|
||||
document.getElementById('template-content').value = '';
|
||||
}
|
||||
|
||||
document.getElementById('template-editor-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
document.getElementById('template-editor-modal').classList.add('hidden');
|
||||
currentTemplate = null;
|
||||
}
|
||||
|
||||
async function loadTemplateContent(templateName) {
|
||||
try {
|
||||
const response = await fetch(`/api/webtemplates/${templateName}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('template-content').value = data.content;
|
||||
updatePreview();
|
||||
} else {
|
||||
showStatus('Failed to load template: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus('Error loading template: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
let name = document.getElementById('template-name').value.trim();
|
||||
const content = document.getElementById('template-content').value;
|
||||
|
||||
if (!name) {
|
||||
showStatus('Template name is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatically add .html extension if not present
|
||||
if (!name.endsWith('.html')) {
|
||||
name = name + '.html';
|
||||
document.getElementById('template-name').value = name;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = currentTemplate ? `/api/webtemplates/${currentTemplate}` : '/api/webtemplates';
|
||||
const method = currentTemplate ? 'PUT' : 'POST';
|
||||
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const csrfToken = '{{ .CSRFToken }}';
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
const body = currentTemplate ?
|
||||
JSON.stringify({ content: content }) :
|
||||
JSON.stringify({ name: name, content: content });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Template saved successfully', 'success');
|
||||
closeEditor();
|
||||
loadTemplates();
|
||||
} else {
|
||||
showStatus('Failed to save template: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus('Error saving template: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateTemplate() {
|
||||
const content = document.getElementById('template-content').value;
|
||||
|
||||
if (!content.trim()) {
|
||||
showValidationResult(false, 'Template content is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const csrfToken = '{{ .CSRFToken }}';
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/webtemplates/validate', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ content: content })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
showValidationResult(data.valid, data.error || 'Template is valid');
|
||||
} catch (error) {
|
||||
showValidationResult(false, 'Error validating template: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showValidationResult(isValid, message) {
|
||||
const resultDiv = document.getElementById('validation-result');
|
||||
resultDiv.className = `mt-2 text-sm ${isValid ? 'text-green-400' : 'text-red-400'}`;
|
||||
resultDiv.textContent = message;
|
||||
resultDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const content = document.getElementById('template-content').value;
|
||||
const preview = document.getElementById('template-preview');
|
||||
|
||||
// Create a blob URL for the preview
|
||||
const blob = new Blob([content], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
preview.src = url;
|
||||
}
|
||||
|
||||
function loadDefaultTemplate() {
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - {{ .ServiceName }}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
|
||||
.login-container { max-width: 400px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
|
||||
button:hover { background: #0056b3; }
|
||||
.error { color: red; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>{{ .ServiceName }}</h2>
|
||||
<form method="POST" action="{{ .LoginPath }}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
document.getElementById('template-content').value = defaultTemplate;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
async function editTemplate(templateName) {
|
||||
openEditor(templateName);
|
||||
}
|
||||
|
||||
async function deleteTemplate(templateName) {
|
||||
if (!confirm(`Are you sure you want to delete the template "${templateName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = {};
|
||||
const csrfToken = '{{ .CSRFToken }}';
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/webtemplates/${templateName}`, {
|
||||
method: 'DELETE',
|
||||
headers: headers
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Template deleted successfully', 'success');
|
||||
loadTemplates();
|
||||
} else {
|
||||
showStatus('Failed to delete template: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus('Error deleting template: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status-message');
|
||||
statusDiv.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-600' : 'bg-red-600'
|
||||
}`;
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
statusDiv.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
57
app/web.go
57
app/web.go
@@ -38,6 +38,7 @@ func initTemplates() error {
|
||||
"templates/settings.html",
|
||||
"templates/threat_reports.html",
|
||||
"templates/threat_rules.html",
|
||||
"templates/webtemplates.html",
|
||||
"templates/users.html",
|
||||
)
|
||||
if err != nil { return err }
|
||||
@@ -71,6 +72,12 @@ func (a *App) startWeb() {
|
||||
|
||||
// Register blocklist export routes (public endpoints for threat intelligence sharing)
|
||||
a.threatManager.GetBlocklistExporter().RegisterExportRoutes(mux, securityManager)
|
||||
|
||||
// Register web template management routes (admin only)
|
||||
a.threatManager.GetWebTemplateAPI().RegisterRoutes(mux, securityManager)
|
||||
|
||||
// Register web services management routes (admin only)
|
||||
a.threatManager.GetWebServicesAPI().RegisterRoutes(mux, securityManager)
|
||||
}
|
||||
|
||||
// Secure dashboard routes with authentication
|
||||
@@ -176,6 +183,10 @@ func (a *App) startWeb() {
|
||||
SIP int `json:"sip"`
|
||||
VNC int `json:"vnc"`
|
||||
} `json:"ports"`
|
||||
Web struct {
|
||||
HTTPTemplateName string `json:"http_template_name"`
|
||||
HTTPSTemplateName string `json:"https_template_name"`
|
||||
} `json:"web"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
@@ -213,11 +224,24 @@ func (a *App) startWeb() {
|
||||
a.cfg.Ports.SIP = in.Ports.SIP
|
||||
a.cfg.Ports.VNC = in.Ports.VNC
|
||||
|
||||
// Update web template settings
|
||||
a.cfg.Web.HTTPTemplateName = in.Web.HTTPTemplateName
|
||||
a.cfg.Web.HTTPSTemplateName = in.Web.HTTPSTemplateName
|
||||
|
||||
// Persist to ./config.json
|
||||
if b, err := json.MarshalIndent(a.cfg, "", " "); err == nil {
|
||||
_ = os.WriteFile("config.json", b, 0644)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status":"ok"})
|
||||
|
||||
// Generate new CSRF token for subsequent requests
|
||||
response := map[string]string{"status": "ok"}
|
||||
if a.threatManager != nil {
|
||||
if newToken, err := a.threatManager.GetSecurityManager().GenerateCSRFToken(); err == nil {
|
||||
response["csrf_token"] = newToken
|
||||
w.Header().Set("X-CSRF-Token", newToken)
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
@@ -227,7 +251,16 @@ func (a *App) startWeb() {
|
||||
// Restart endpoint: triggers app restart
|
||||
mux.HandleFunc("/api/restart", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return }
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status":"restarting"})
|
||||
|
||||
// Generate new CSRF token for subsequent requests
|
||||
response := map[string]string{"status": "restarting"}
|
||||
if a.threatManager != nil {
|
||||
if newToken, err := a.threatManager.GetSecurityManager().GenerateCSRFToken(); err == nil {
|
||||
response["csrf_token"] = newToken
|
||||
w.Header().Set("X-CSRF-Token", newToken)
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
go func(){ time.Sleep(700*time.Millisecond); a.Restart() }()
|
||||
})))
|
||||
mux.HandleFunc("/logs", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -374,7 +407,7 @@ func (a *App) startWeb() {
|
||||
http.Error(w, "templates not loaded", 500)
|
||||
}))
|
||||
|
||||
// Users page (Admin only)
|
||||
// Role middleware setup for admin pages
|
||||
var roleMiddleware func(string) func(http.HandlerFunc) http.HandlerFunc
|
||||
if a.threatManager != nil {
|
||||
roleMiddleware = a.threatManager.GetSecurityManager().RoleMiddleware
|
||||
@@ -385,6 +418,24 @@ func (a *App) startWeb() {
|
||||
}
|
||||
}
|
||||
|
||||
// Web Templates page (Admin only)
|
||||
mux.HandleFunc("/webtemplates", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]any{
|
||||
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
|
||||
"PageTitle": "webtemplates_title",
|
||||
"PageContent": "webtemplates_content",
|
||||
}
|
||||
// Add CSRF token if security manager is available
|
||||
if a.threatManager != nil {
|
||||
a.threatManager.GetSecurityManager().AddCSRFToken(w, data)
|
||||
}
|
||||
if templates != nil {
|
||||
_ = templates.ExecuteTemplate(w, "layout.html", data)
|
||||
return
|
||||
}
|
||||
http.Error(w, "templates not loaded", 500)
|
||||
})))
|
||||
|
||||
mux.HandleFunc("/users", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get current user from context
|
||||
var currentUser interface{}
|
||||
|
||||
241
app/webservice_handler.go
Normal file
241
app/webservice_handler.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebServiceHandler handles custom web honeypot services
|
||||
type WebServiceHandler struct {
|
||||
config WebServiceConfig
|
||||
templateManager *WebTemplateManager
|
||||
logFunc func(Record)
|
||||
}
|
||||
|
||||
// NewWebServiceHandler creates a new web service handler
|
||||
func NewWebServiceHandler(config WebServiceConfig, templateManager *WebTemplateManager, logFunc func(Record)) *WebServiceHandler {
|
||||
return &WebServiceHandler{
|
||||
config: config,
|
||||
templateManager: templateManager,
|
||||
logFunc: logFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface
|
||||
func (wsh *WebServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Log all requests
|
||||
wsh.logRequest(r)
|
||||
|
||||
// Check if request is for the configured path
|
||||
if r.URL.Path == wsh.config.Path {
|
||||
wsh.handleLoginPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For all other paths, return 403 Forbidden
|
||||
wsh.logEvent(r, "forbidden_access", map[string]string{
|
||||
"requested_path": r.URL.Path,
|
||||
"configured_path": wsh.config.Path,
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
|
||||
// handleLoginPage serves the login page and processes login attempts
|
||||
func (wsh *WebServiceHandler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
wsh.serveLoginForm(w, r)
|
||||
case http.MethodPost:
|
||||
wsh.handleLoginAttempt(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// serveLoginForm renders and serves the login form
|
||||
func (wsh *WebServiceHandler) serveLoginForm(w http.ResponseWriter, r *http.Request) {
|
||||
// Load template
|
||||
tmpl, err := wsh.templateManager.LoadTemplate(wsh.config.TemplateName)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load template %s: %v", wsh.config.TemplateName, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare template data
|
||||
data := map[string]interface{}{
|
||||
"ServiceName": wsh.config.Name,
|
||||
"LoginPath": wsh.config.Path,
|
||||
"UseHTTPS": wsh.config.UseHTTPS,
|
||||
}
|
||||
|
||||
// Set content type
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Execute template
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("Failed to execute template: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log page view
|
||||
wsh.logEvent(r, "login_page_view", map[string]string{
|
||||
"template": wsh.config.TemplateName,
|
||||
})
|
||||
}
|
||||
|
||||
// handleLoginAttempt processes login form submissions
|
||||
func (wsh *WebServiceHandler) handleLoginAttempt(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
|
||||
// Log the login attempt
|
||||
wsh.logEvent(r, "login_attempt", map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"template": wsh.config.TemplateName,
|
||||
})
|
||||
|
||||
// Always reject login attempts (this is a honeypot)
|
||||
// Redirect back to login page with error parameter
|
||||
redirectURL := fmt.Sprintf("%s?error=invalid", wsh.config.Path)
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// logRequest logs basic request information
|
||||
func (wsh *WebServiceHandler) logRequest(r *http.Request) {
|
||||
details := map[string]string{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"query": r.URL.RawQuery,
|
||||
"user_agent": r.Header.Get("User-Agent"),
|
||||
"referer": r.Header.Get("Referer"),
|
||||
}
|
||||
|
||||
// Add additional headers that might be interesting
|
||||
if auth := r.Header.Get("Authorization"); auth != "" {
|
||||
details["authorization"] = auth
|
||||
}
|
||||
if cookie := r.Header.Get("Cookie"); cookie != "" {
|
||||
details["cookie"] = cookie
|
||||
}
|
||||
|
||||
wsh.logEvent(r, "http_request", details)
|
||||
}
|
||||
|
||||
// logEvent logs an event with the web service context
|
||||
func (wsh *WebServiceHandler) logEvent(r *http.Request, eventType string, details map[string]string) {
|
||||
if details == nil {
|
||||
details = make(map[string]string)
|
||||
}
|
||||
|
||||
// Add service context
|
||||
details["event"] = eventType
|
||||
details["service_name"] = wsh.config.Name
|
||||
details["service_port"] = strconv.Itoa(wsh.config.Port)
|
||||
details["service_path"] = wsh.config.Path
|
||||
|
||||
// Create log record
|
||||
record := Record{
|
||||
Timestamp: time.Now().UTC(),
|
||||
RemoteAddr: remoteIP(r.RemoteAddr),
|
||||
RemotePort: remotePort(r.RemoteAddr),
|
||||
Service: fmt.Sprintf("web-%s", wsh.config.Name),
|
||||
Details: details,
|
||||
RawPayload: fmt.Sprintf("%s %s", r.Method, r.URL.String()),
|
||||
}
|
||||
|
||||
if wsh.logFunc != nil {
|
||||
wsh.logFunc(record)
|
||||
}
|
||||
}
|
||||
|
||||
// WebServiceManager manages multiple web service instances
|
||||
type WebServiceManager struct {
|
||||
templateManager *WebTemplateManager
|
||||
logFunc func(Record)
|
||||
services map[string]*WebServiceHandler
|
||||
}
|
||||
|
||||
// NewWebServiceManager creates a new web service manager
|
||||
func NewWebServiceManager(templateManager *WebTemplateManager, logFunc func(Record)) *WebServiceManager {
|
||||
return &WebServiceManager{
|
||||
templateManager: templateManager,
|
||||
logFunc: logFunc,
|
||||
services: make(map[string]*WebServiceHandler),
|
||||
}
|
||||
}
|
||||
|
||||
// StartWebService starts a web service on the specified configuration
|
||||
func (wsm *WebServiceManager) StartWebService(config WebServiceConfig) error {
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create handler
|
||||
handler := NewWebServiceHandler(config, wsm.templateManager, wsm.logFunc)
|
||||
|
||||
// Create HTTP server
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", handler)
|
||||
|
||||
addr := fmt.Sprintf(":%d", config.Port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Store service
|
||||
serviceKey := fmt.Sprintf("%s:%d", config.Name, config.Port)
|
||||
wsm.services[serviceKey] = handler
|
||||
|
||||
log.Printf("Starting web service '%s' on port %d at path %s", config.Name, config.Port, config.Path)
|
||||
|
||||
// Start server
|
||||
go func() {
|
||||
var err error
|
||||
if config.UseHTTPS {
|
||||
// For HTTPS, we would need to load certificates; using HTTP placeholder
|
||||
log.Printf("HTTPS requested for %s but using HTTP for now. Configure TLS certificates for production.", config.Name)
|
||||
err = server.ListenAndServe()
|
||||
} else {
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("Web service %s error: %v", config.Name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopWebService stops a web service
|
||||
func (wsm *WebServiceManager) StopWebService(name string, port int) {
|
||||
serviceKey := fmt.Sprintf("%s:%d", name, port)
|
||||
delete(wsm.services, serviceKey)
|
||||
// Note: In a production system, you'd want to properly shutdown the HTTP server
|
||||
}
|
||||
|
||||
// GetActiveServices returns a list of active web services
|
||||
func (wsm *WebServiceManager) GetActiveServices() []string {
|
||||
var services []string
|
||||
for key := range wsm.services {
|
||||
services = append(services, key)
|
||||
}
|
||||
return services
|
||||
}
|
||||
311
app/webtemplate_manager.go
Normal file
311
app/webtemplate_manager.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WebTemplateManager handles HTML template management for web honeypots
|
||||
type WebTemplateManager struct {
|
||||
templateDir string
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
// NewWebTemplateManager creates a new template manager
|
||||
func NewWebTemplateManager(configPath string) *WebTemplateManager {
|
||||
templateDir := filepath.Join(filepath.Dir(configPath), "webtemplates")
|
||||
|
||||
// Ensure template directory exists
|
||||
os.MkdirAll(templateDir, 0755)
|
||||
|
||||
return &WebTemplateManager{
|
||||
templateDir: templateDir,
|
||||
templates: make(map[string]*template.Template),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplateDir returns the template directory path
|
||||
func (wtm *WebTemplateManager) GetTemplateDir() string {
|
||||
return wtm.templateDir
|
||||
}
|
||||
|
||||
// CreateDefaultTemplate creates a default login template if it doesn't exist
|
||||
func (wtm *WebTemplateManager) CreateDefaultTemplate(templateName string) error {
|
||||
templatePath := filepath.Join(wtm.templateDir, templateName)
|
||||
|
||||
// Check if template already exists
|
||||
if _, err := os.Stat(templatePath); err == nil {
|
||||
return nil // Template already exists
|
||||
}
|
||||
|
||||
defaultHTML := `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - {{ .ServiceName }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.login-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 0.75rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>{{ .ServiceName }}</h1>
|
||||
<p>Please sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="error-message">
|
||||
Invalid username or password
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ .LoginPath }}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">Sign In</button>
|
||||
</form>
|
||||
|
||||
<div class="footer">
|
||||
<p>© 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
|
||||
}
|
||||
136
webtemplates/admin-login.html
Normal file
136
webtemplates/admin-login.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - {{ .ServiceName }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.login-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 0.75rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>{{ .ServiceName }}</h1>
|
||||
<p>Please sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="error-message">
|
||||
Invalid username or password
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ .LoginPath }}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">Sign In</button>
|
||||
</form>
|
||||
|
||||
<div class="footer">
|
||||
<p>© 2024 {{ .ServiceName }}. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Show error message if login failed
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('error') === 'invalid') {
|
||||
document.getElementById('error-message').style.display = 'block';
|
||||
}
|
||||
|
||||
// Focus on username field
|
||||
document.getElementById('username').focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
webtemplates/test.html
Normal file
34
webtemplates/test.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - </title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
|
||||
.login-container { max-width: 400px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
|
||||
button:hover { background: #0056b3; }
|
||||
.error { color: red; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2></h2>
|
||||
<form method="POST" action="">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user