frontend with settings and restarting app

This commit is contained in:
2025-09-28 14:47:22 +01:00
parent 58d870ba46
commit 8235d7eedd
11 changed files with 699 additions and 150 deletions

View File

@@ -1,138 +1,153 @@
package app
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// Config contains runtime configuration for the honeypot
type Config struct {
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
LogPath string `json:"log_path"`
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"`
} `json:"web"`
Web struct {
Enabled bool `json:"enabled"`
Bind string `json:"bind"`
Port int `json:"port"`
} `json:"web"`
Services struct {
HTTP bool `json:"http"`
HTTPS bool `json:"https"`
SSH bool `json:"ssh"`
FTP bool `json:"ftp"`
SMTP bool `json:"smtp"`
IMAP bool `json:"imap"`
Telnet bool `json:"telnet"`
MySQL bool `json:"mysql"`
PostgreSQL bool `json:"postgresql"`
MongoDB bool `json:"mongodb"`
RDP bool `json:"rdp"`
SMB bool `json:"smb"`
SIP bool `json:"sip"`
VNC bool `json:"vnc"`
Generic []int `json:"generic"`
} `json:"services"`
Services struct {
HTTP bool `json:"http"`
HTTPS bool `json:"https"`
SSH bool `json:"ssh"`
FTP bool `json:"ftp"`
SMTP bool `json:"smtp"`
IMAP bool `json:"imap"`
Telnet bool `json:"telnet"`
MySQL bool `json:"mysql"`
PostgreSQL bool `json:"postgresql"`
MongoDB bool `json:"mongodb"`
RDP bool `json:"rdp"`
SMB bool `json:"smb"`
SIP bool `json:"sip"`
VNC bool `json:"vnc"`
Generic []int `json:"generic"`
} `json:"services"`
Ports struct {
HTTP int `json:"http"`
HTTPS int `json:"https"`
SSH int `json:"ssh"`
FTP int `json:"ftp"`
SMTP int `json:"smtp"`
IMAP int `json:"imap"`
Telnet int `json:"telnet"`
MySQL int `json:"mysql"`
PostgreSQL int `json:"postgresql"`
MongoDB int `json:"mongodb"`
RDP int `json:"rdp"`
SMB int `json:"smb"`
SIP int `json:"sip"`
VNC int `json:"vnc"`
} `json:"ports"`
Ports struct {
HTTP int `json:"http"`
HTTPS int `json:"https"`
SSH int `json:"ssh"`
FTP int `json:"ftp"`
SMTP int `json:"smtp"`
IMAP int `json:"imap"`
Telnet int `json:"telnet"`
MySQL int `json:"mysql"`
PostgreSQL int `json:"postgresql"`
MongoDB int `json:"mongodb"`
RDP int `json:"rdp"`
SMB int `json:"smb"`
SIP int `json:"sip"`
VNC int `json:"vnc"`
} `json:"ports"`
// Certificates allows overriding default certificate/key locations.
Certificates struct {
// SSHHostKeyPath points to a PEM-encoded RSA private key to use as SSH host key.
// If empty, a persistent key will be created in the same directory as LogPath.
SSHHostKeyPath string `json:"ssh_host_key_path"`
// TLSCertPath and TLSKeyPath are used by TLS-capable services if provided.
// If empty, a self-signed certificate will be generated and stored next to LogPath.
TLSCertPath string `json:"tls_cert_path"`
TLSKeyPath string `json:"tls_key_path"`
} `json:"certificates"`
// Certificates allows overriding default certificate/key locations.
Certificates struct {
// SSHHostKeyPath points to a PEM-encoded RSA private key to use as SSH host key.
// If empty, a persistent key will be created in the same directory as LogPath.
SSHHostKeyPath string `json:"ssh_host_key_path"`
// TLSCertPath and TLSKeyPath are used by TLS-capable services if provided.
// If empty, a self-signed certificate will be generated and stored next to LogPath.
TLSCertPath string `json:"tls_cert_path"`
TLSKeyPath string `json:"tls_key_path"`
} `json:"certificates"`
}
// LastConfigPath holds the last path used to load/save config.json
var LastConfigPath string
// EnsureConfig writes a default config file if the given path doesn't exist
func EnsureConfig(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("create config dir: %w", err)
}
def := defaultConfig()
b, _ := json.MarshalIndent(def, "", " ")
if err := os.WriteFile(path, b, 0644); err != nil {
return fmt.Errorf("write default config: %w", err)
}
}
return nil
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("create config dir: %w", err)
}
def := defaultConfig()
b, _ := json.MarshalIndent(def, "", " ")
if err := os.WriteFile(path, b, 0644); err != nil {
return fmt.Errorf("write default config: %w", err)
}
}
return nil
}
// LoadConfig loads JSON config from path
func LoadConfig(path string) (Config, error) {
var cfg Config
b, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := json.Unmarshal(b, &cfg); err != nil {
return cfg, err
}
return cfg, nil
var cfg Config
b, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := json.Unmarshal(b, &cfg); err != nil {
return cfg, err
}
LastConfigPath = path
return cfg, nil
}
// SaveConfig writes the given config to the provided path
func SaveConfig(path string, cfg Config) error {
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, b, 0644)
}
func defaultConfig() Config {
var c Config
c.LogMode = "file"
c.LogPath = "honeypot.log"
c.Web.Enabled = true
c.Web.Bind = "127.0.0.1"
c.Web.Port = 6333
var c Config
c.LogMode = "file"
c.LogPath = "honeypot.log"
c.Web.Enabled = true
c.Web.Bind = "127.0.0.1"
c.Web.Port = 6333
// Enable common services by default
c.Services.HTTP = true
c.Services.HTTPS = false
c.Services.SSH = true
c.Services.FTP = true
c.Services.SMTP = true
c.Services.Telnet = true
c.Services.MySQL = false
c.Services.PostgreSQL = false
c.Services.MongoDB = false
c.Services.IMAP = false
c.Services.RDP = false
c.Services.SMB = false
c.Services.SIP = false
c.Services.VNC = false
c.Services.Generic = []int{}
// Enable common services by default
c.Services.HTTP = true
c.Services.HTTPS = false
c.Services.SSH = true
c.Services.FTP = true
c.Services.SMTP = true
c.Services.Telnet = true
c.Services.MySQL = false
c.Services.PostgreSQL = false
c.Services.MongoDB = false
c.Services.IMAP = false
c.Services.RDP = false
c.Services.SMB = false
c.Services.SIP = false
c.Services.VNC = false
c.Services.Generic = []int{}
// Standard ports
c.Ports.HTTP = 8080
c.Ports.HTTPS = 8443
c.Ports.SSH = 2222
c.Ports.FTP = 2121
c.Ports.SMTP = 2525
c.Ports.IMAP = 1143
c.Ports.Telnet = 2323
c.Ports.MySQL = 3306
c.Ports.PostgreSQL = 5432
c.Ports.MongoDB = 27017
c.Ports.RDP = 3389
c.Ports.SMB = 4450
c.Ports.SIP = 5060
c.Ports.VNC = 5900
// Standard ports
c.Ports.HTTP = 8080
c.Ports.HTTPS = 8443
c.Ports.SSH = 2222
c.Ports.FTP = 2121
c.Ports.SMTP = 2525
c.Ports.IMAP = 1143
c.Ports.Telnet = 2323
c.Ports.MySQL = 3306
c.Ports.PostgreSQL = 5432
c.Ports.MongoDB = 27017
c.Ports.RDP = 3389
c.Ports.SIP = 5060
c.Ports.VNC = 5900
return c
return c
}

View File

@@ -41,6 +41,8 @@ type App struct {
mu sync.Mutex
listeners []net.Listener
conns map[net.Conn]struct{}
// restart channel
restartCh chan struct{}
}
// HTTPS honeypot using self-signed or configured certificate
@@ -203,7 +205,7 @@ func NewApp(cfg Config) (*App, error) {
// Root context for the App used for shutdown signalling
ctx, cancel := context.WithCancel(context.Background())
a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{})}
a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1)}
return a, nil
}
@@ -331,6 +333,17 @@ func (a *App) Run(ctx context.Context) error {
return nil
}
func (a *App) Restart() {
select {
case a.restartCh <- struct{}{}:
default:
}
}
func (a *App) RestartChan() <-chan struct{} {
return a.restartCh
}
func (a *App) Shutdown() {
a.cancel()
// attempt to close all http servers if running

View File

@@ -0,0 +1,31 @@
{{ define "blacklist_title" }}Blacklist{{ end }}
{{ define "blacklist_content" }}
<h1 class="text-2xl font-semibold text-white mb-4">Blacklisted IPs</h1>
<div class="bg-gray-800 border border-gray-700 rounded-lg">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-800">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">IP Address</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-gray-900 divide-y divide-gray-800">
{{ range .Blacklisted }}
<tr>
<td class="px-4 py-2 text-sm text-gray-300">{{ . }}</td>
<td class="px-4 py-2 text-sm text-gray-300">
<!-- Actions could be added here (unblacklist) -->
<span class="text-gray-500"></span>
</td>
</tr>
{{ else }}
<tr>
<td class="px-4 py-4 text-sm text-gray-400" colspan="2">No blacklisted IPs.</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ end }}

View File

@@ -1,5 +1,5 @@
{{ define "title" }}Honeypot Dashboard{{ end }}
{{ define "content" }}
{{ define "index_title" }}Honeypot Dashboard{{ end }}
{{ define "index_content" }}
<div class="space-y-8">
<h1 class="text-2xl font-semibold text-white">Honeypot Overview</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
@@ -31,4 +31,3 @@
</div>
</div>
{{ end }}
{{ define "index" }}{{ template "layout.html" . }}{{ end }}

View File

@@ -3,7 +3,15 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ block "title" . }}Honeypot Dashboard{{ end }}</title>
<title>
{{ if eq .PageTitle "index_title" }}Honeypot Dashboard
{{ else if eq .PageTitle "logs_title" }}Recent Logs
{{ else if eq .PageTitle "settings_title" }}Settings
{{ else if eq .PageTitle "stats_title" }}Statistics
{{ else if eq .PageTitle "blacklist_title" }}Blacklist
{{ else if eq .PageTitle "threats_title" }}Top Threats
{{ else }}Honeypot Dashboard{{ end }}
</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
@@ -27,18 +35,31 @@
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center space-x-8">
<a href="/" class="text-primary-400 hover:text-primary-300 font-semibold">🍯 Honeypot</a>
<a href="/" class="text-primary-400 hover:text-primary-300 font-semibold">Honeypot Dashboard</a>
<a href="/logs" class="text-gray-300 hover:text-white">Recent Logs</a>
<a href="/threats" class="text-gray-300 hover:text-white">Top Threats</a>
<a href="/blacklist" class="text-gray-300 hover:text-white">Blacklist</a>
<a href="/stats" class="text-gray-300 hover:text-white">Statistics</a>
<a href="/settings" class="text-gray-300 hover:text-white">Settings</a>
</div>
<div class="text-xs text-gray-400">{{ .Now }}</div>
</div>
</div>
</nav>
<main class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
{{ block "content" . }}{{ end }}
{{ if eq .PageContent "index_content" }}
{{ template "index_content" . }}
{{ else if eq .PageContent "logs_content" }}
{{ template "logs_content" . }}
{{ else if eq .PageContent "settings_content" }}
{{ template "settings_content" . }}
{{ else if eq .PageContent "stats_content" }}
{{ template "stats_content" . }}
{{ else if eq .PageContent "blacklist_content" }}
{{ template "blacklist_content" . }}
{{ else if eq .PageContent "threats_content" }}
{{ template "threats_content" . }}
{{ end }}
</main>
<footer class="border-t border-gray-800 py-6 text-center text-gray-500 text-sm">Honeypot Dashboard</footer>
</div>

View File

@@ -1,5 +1,5 @@
{{ define "title" }}Recent Logs{{ end }}
{{ define "content" }}
{{ define "logs_title" }}Recent Logs{{ end }}
{{ define "logs_content" }}
<h1 class="text-2xl font-semibold text-white mb-4">Recent Logs</h1>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
@@ -26,4 +26,3 @@
</table>
</div>
{{ end }}
{{ define "logs" }}{{ template "layout.html" . }}{{ end }}

193
app/templates/settings.html Normal file
View File

@@ -0,0 +1,193 @@
{{ define "settings_title" }}Settings{{ end }}
{{ define "settings_content" }}
<h1 class="text-2xl font-semibold text-white mb-6">Settings</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-gray-800 border border-gray-700 rounded-lg p-6">
<h2 class="text-lg font-semibold text-white mb-4">Services</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{{/* Toggle component */}}
{{ $svc := .Cfg.Services }}
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">HTTP</span>
<input id="svc-http" type="checkbox" class="h-5 w-5" {{ if $svc.HTTP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">HTTPS</span>
<input id="svc-https" type="checkbox" class="h-5 w-5" {{ if $svc.HTTPS }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">SSH</span>
<input id="svc-ssh" type="checkbox" class="h-5 w-5" {{ if $svc.SSH }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">FTP</span>
<input id="svc-ftp" type="checkbox" class="h-5 w-5" {{ if $svc.FTP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">SMTP</span>
<input id="svc-smtp" type="checkbox" class="h-5 w-5" {{ if $svc.SMTP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">IMAP</span>
<input id="svc-imap" type="checkbox" class="h-5 w-5" {{ if $svc.IMAP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">Telnet</span>
<input id="svc-telnet" type="checkbox" class="h-5 w-5" {{ if $svc.Telnet }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">MySQL</span>
<input id="svc-mysql" type="checkbox" class="h-5 w-5" {{ if $svc.MySQL }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">PostgreSQL</span>
<input id="svc-postgresql" type="checkbox" class="h-5 w-5" {{ if $svc.PostgreSQL }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">MongoDB</span>
<input id="svc-mongodb" type="checkbox" class="h-5 w-5" {{ if $svc.MongoDB }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">RDP</span>
<input id="svc-rdp" type="checkbox" class="h-5 w-5" {{ if $svc.RDP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">SMB</span>
<input id="svc-smb" type="checkbox" class="h-5 w-5" {{ if $svc.SMB }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">SIP</span>
<input id="svc-sip" type="checkbox" class="h-5 w-5" {{ if $svc.SIP }}checked{{ end }}>
</label>
<label class="flex items-center justify-between bg-gray-900 border border-gray-700 rounded p-3">
<span class="text-gray-200">VNC</span>
<input id="svc-vnc" type="checkbox" class="h-5 w-5" {{ if $svc.VNC }}checked{{ end }}>
</label>
</div>
</div>
<div class="bg-gray-800 border border-gray-700 rounded-lg p-6">
<h2 class="text-lg font-semibold text-white mb-4">Ports</h2>
{{ $p := .Cfg.Ports }}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label class="block">
<span class="text-gray-300">HTTP</span>
<input id="port-http" type="number" value="{{ $p.HTTP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">HTTPS</span>
<input id="port-https" type="number" value="{{ $p.HTTPS }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">SSH</span>
<input id="port-ssh" type="number" value="{{ $p.SSH }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">FTP</span>
<input id="port-ftp" type="number" value="{{ $p.FTP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">SMTP</span>
<input id="port-smtp" type="number" value="{{ $p.SMTP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">IMAP</span>
<input id="port-imap" type="number" value="{{ $p.IMAP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">Telnet</span>
<input id="port-telnet" type="number" value="{{ $p.Telnet }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">MySQL</span>
<input id="port-mysql" type="number" value="{{ $p.MySQL }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">PostgreSQL</span>
<input id="port-postgresql" type="number" value="{{ $p.PostgreSQL }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">MongoDB</span>
<input id="port-mongodb" type="number" value="{{ $p.MongoDB }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">RDP</span>
<input id="port-rdp" type="number" value="{{ $p.RDP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">SMB</span>
<input id="port-smb" type="number" value="{{ $p.SMB }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">SIP</span>
<input id="port-sip" type="number" value="{{ $p.SIP }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
<label class="block">
<span class="text-gray-300">VNC</span>
<input id="port-vnc" type="number" value="{{ $p.VNC }}" class="mt-1 w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100" />
</label>
</div>
</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>
async function saveSettings() {
const payload = {
services: {
http: document.getElementById('svc-http').checked,
https: document.getElementById('svc-https').checked,
ssh: document.getElementById('svc-ssh').checked,
ftp: document.getElementById('svc-ftp').checked,
smtp: document.getElementById('svc-smtp').checked,
imap: document.getElementById('svc-imap').checked,
telnet: document.getElementById('svc-telnet').checked,
mysql: document.getElementById('svc-mysql').checked,
postgresql: document.getElementById('svc-postgresql').checked,
mongodb: document.getElementById('svc-mongodb').checked,
rdp: document.getElementById('svc-rdp').checked,
smb: document.getElementById('svc-smb').checked,
sip: document.getElementById('svc-sip').checked,
vnc: document.getElementById('svc-vnc').checked,
},
ports: {
http: parseInt(document.getElementById('port-http').value, 10),
https: parseInt(document.getElementById('port-https').value, 10),
ssh: parseInt(document.getElementById('port-ssh').value, 10),
ftp: parseInt(document.getElementById('port-ftp').value, 10),
smtp: parseInt(document.getElementById('port-smtp').value, 10),
imap: parseInt(document.getElementById('port-imap').value, 10),
telnet: parseInt(document.getElementById('port-telnet').value, 10),
mysql: parseInt(document.getElementById('port-mysql').value, 10),
postgresql: parseInt(document.getElementById('port-postgresql').value, 10),
mongodb: parseInt(document.getElementById('port-mongodb').value, 10),
rdp: parseInt(document.getElementById('port-rdp').value, 10),
smb: parseInt(document.getElementById('port-smb').value, 10),
sip: parseInt(document.getElementById('port-sip').value, 10),
vnc: parseInt(document.getElementById('port-vnc').value, 10),
}
};
const res = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const out = await res.json().catch(() => ({}));
const el = document.getElementById('save-status');
if (res.ok) {
el.textContent = 'Saved. You may need to restart to apply port changes.';
el.className = 'text-sm text-green-400';
} else {
el.textContent = out.error || 'Save failed';
el.className = 'text-sm text-red-400';
}
}
async function restartApp() {
const res = await fetch('/api/restart', { method: 'POST' });
if (res.ok) {
document.getElementById('save-status').textContent = 'Restarting...';
setTimeout(() => location.reload(), 1200);
}
}
document.getElementById('btn-save').addEventListener('click', saveSettings);
document.getElementById('btn-restart').addEventListener('click', restartApp);
</script>
{{ end }}

47
app/templates/stats.html Normal file
View File

@@ -0,0 +1,47 @@
{{ define "stats_title" }}Statistics{{ end }}
{{ define "stats_content" }}
<h1 class="text-2xl font-semibold text-white mb-4">Statistics</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div class="text-sm text-gray-400">Total IPs</div>
<div class="text-3xl font-bold text-primary-400">{{ index .Stats "total_ips" }}</div>
</div>
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div class="text-sm text-gray-400">Blacklisted</div>
<div class="text-3xl font-bold text-primary-400">{{ index .Stats "blacklisted_ips" }}</div>
</div>
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div class="text-sm text-gray-400">Connections</div>
<div class="text-3xl font-bold text-primary-400">{{ index .Stats "total_connections" }}</div>
</div>
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div class="text-sm text-gray-400">Auth Attempts</div>
<div class="text-3xl font-bold text-primary-400">{{ index .Stats "total_auth_attempts" }}</div>
</div>
</div>
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
<h2 class="text-xl font-semibold text-white mb-3">Service Activity</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-800">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Service</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Connections</th>
</tr>
</thead>
<tbody class="bg-gray-900 divide-y divide-gray-800">
{{ range $svc, $cnt := .ServiceStats }}
<tr>
<td class="px-4 py-2 text-sm text-gray-300">{{ $svc }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ $cnt }}</td>
</tr>
{{ else }}
<tr>
<td class="px-4 py-4 text-sm text-gray-400" colspan="2">No data.</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,32 @@
{{ define "threats_title" }}Top Threats{{ end }}
{{ define "threats_content" }}
<h1 class="text-2xl font-semibold text-white mb-4">Top Threats</h1>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-800">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">IP</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Threat Score</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Connections</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Auth Attempts</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Services</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Last Seen</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Blacklisted</th>
</tr>
</thead>
<tbody class="bg-gray-900 divide-y divide-gray-800">
{{ range .Threats }}
<tr>
<td class="px-4 py-2 text-sm text-gray-300">{{ .IP }}</td>
<td class="px-4 py-2 text-sm text-gray-300 font-semibold">{{ .ThreatScore }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ .TotalConnections }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ .AuthAttempts }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ len .Services }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ .LastSeen.Format "2006-01-02 15:04:05" }}</td>
<td class="px-4 py-2 text-sm text-gray-300">{{ if .IsBlacklisted }}<span class="text-red-400">Yes</span>{{ else }}No{{ end }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}

View File

@@ -25,7 +25,16 @@ func initTemplates() error {
},
}
// Parse layout first, then pages
t, err := template.New("layout.html").Funcs(funcMap).ParseFS(templatesFS, "templates/layout.html", "templates/index.html", "templates/logs.html")
t, err := template.New("layout.html").Funcs(funcMap).ParseFS(
templatesFS,
"templates/layout.html",
"templates/index.html",
"templates/logs.html",
"templates/threats.html",
"templates/blacklist.html",
"templates/stats.html",
"templates/settings.html",
)
if err != nil { return err }
templates = t
return nil
@@ -49,13 +58,125 @@ func (a *App) startWeb() {
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Stats": stats,
"PageTitle": "index_title",
"PageContent": "index_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "index", data)
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})
// Settings UI
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Cfg": a.cfg,
"PageTitle": "settings_title",
"PageContent": "settings_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})
// API to read/update settings (services + ports)
mux.HandleFunc("/api/settings", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
_ = json.NewEncoder(w).Encode(a.cfg)
return
case http.MethodPost:
var in struct {
Services struct {
HTTP bool `json:"http"`
HTTPS bool `json:"https"`
SSH bool `json:"ssh"`
FTP bool `json:"ftp"`
SMTP bool `json:"smtp"`
IMAP bool `json:"imap"`
Telnet bool `json:"telnet"`
MySQL bool `json:"mysql"`
PostgreSQL bool `json:"postgresql"`
MongoDB bool `json:"mongodb"`
RDP bool `json:"rdp"`
SMB bool `json:"smb"`
SIP bool `json:"sip"`
VNC bool `json:"vnc"`
} `json:"services"`
Ports struct {
HTTP int `json:"http"`
HTTPS int `json:"https"`
SSH int `json:"ssh"`
FTP int `json:"ftp"`
SMTP int `json:"smtp"`
IMAP int `json:"imap"`
Telnet int `json:"telnet"`
MySQL int `json:"mysql"`
PostgreSQL int `json:"postgresql"`
MongoDB int `json:"mongodb"`
RDP int `json:"rdp"`
SMB int `json:"smb"`
SIP int `json:"sip"`
VNC int `json:"vnc"`
} `json:"ports"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error":"bad json"})
return
}
// Update in-memory
a.cfg.Services.HTTP = in.Services.HTTP
a.cfg.Services.HTTPS = in.Services.HTTPS
a.cfg.Services.SSH = in.Services.SSH
a.cfg.Services.FTP = in.Services.FTP
a.cfg.Services.SMTP = in.Services.SMTP
a.cfg.Services.IMAP = in.Services.IMAP
a.cfg.Services.Telnet = in.Services.Telnet
a.cfg.Services.MySQL = in.Services.MySQL
a.cfg.Services.PostgreSQL = in.Services.PostgreSQL
a.cfg.Services.MongoDB = in.Services.MongoDB
a.cfg.Services.RDP = in.Services.RDP
a.cfg.Services.SMB = in.Services.SMB
a.cfg.Services.SIP = in.Services.SIP
a.cfg.Services.VNC = in.Services.VNC
a.cfg.Ports.HTTP = in.Ports.HTTP
a.cfg.Ports.HTTPS = in.Ports.HTTPS
a.cfg.Ports.SSH = in.Ports.SSH
a.cfg.Ports.FTP = in.Ports.FTP
a.cfg.Ports.SMTP = in.Ports.SMTP
a.cfg.Ports.IMAP = in.Ports.IMAP
a.cfg.Ports.Telnet = in.Ports.Telnet
a.cfg.Ports.MySQL = in.Ports.MySQL
a.cfg.Ports.PostgreSQL = in.Ports.PostgreSQL
a.cfg.Ports.MongoDB = in.Ports.MongoDB
a.cfg.Ports.RDP = in.Ports.RDP
a.cfg.Ports.SMB = in.Ports.SMB
a.cfg.Ports.SIP = in.Ports.SIP
a.cfg.Ports.VNC = in.Ports.VNC
// 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"})
return
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
})
// Restart endpoint: triggers app restart
mux.HandleFunc("/api/restart", 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"})
go func(){ time.Sleep(700*time.Millisecond); a.Restart() }()
})
mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
// display last 200 logs
var rows []Record
@@ -102,9 +223,67 @@ func (a *App) startWeb() {
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Rows": rows,
"PageTitle": "logs_title",
"PageContent": "logs_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "logs", data)
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})
mux.HandleFunc("/threats", func(w http.ResponseWriter, r *http.Request) {
var threats []*IPThreatInfo
if a.threatIntel != nil {
threats = a.threatIntel.GetTopThreats(50)
}
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Threats": threats,
"PageTitle": "threats_title",
"PageContent": "threats_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})
mux.HandleFunc("/blacklist", func(w http.ResponseWriter, r *http.Request) {
var bl []string
if a.threatIntel != nil {
bl = a.threatIntel.GetBlacklistedIPs()
}
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Blacklisted": bl,
"PageTitle": "blacklist_title",
"PageContent": "blacklist_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)
})
mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
stats := map[string]any{}
var svc map[string]int
if a.threatIntel != nil {
stats = a.threatIntel.GetStats()
if v, ok := stats["service_stats"].(map[string]int); ok {
svc = v
}
}
data := map[string]any{
"Now": time.Now().Format("2006-01-02 15:04:05 MST"),
"Stats": stats,
"ServiceStats": svc,
"PageTitle": "stats_title",
"PageContent": "stats_content",
}
if templates != nil {
_ = templates.ExecuteTemplate(w, "layout.html", data)
return
}
http.Error(w, "templates not loaded", 500)

70
main.go
View File

@@ -8,6 +8,7 @@ import (
"os"
"os/signal"
"syscall"
"time"
"honeydany/app"
)
@@ -16,36 +17,55 @@ func main() {
cfgPath := flag.String("config", "config.json", "path to config.json")
flag.Parse()
// ensure config exists (auto-generate default if missing)
if err := app.EnsureConfig(*cfgPath); err != nil {
log.Fatalf("ensure config: %v", err)
}
for {
// ensure config exists (auto-generate default if missing)
if err := app.EnsureConfig(*cfgPath); err != nil {
log.Fatalf("ensure config: %v", err)
}
cfg, err := app.LoadConfig(*cfgPath)
if err != nil {
log.Fatalf("load config: %v", err)
}
cfg, err := app.LoadConfig(*cfgPath)
if err != nil {
log.Fatalf("load config: %v", err)
}
a, err := app.NewApp(cfg)
if err != nil {
log.Fatalf("create app: %v", err)
}
a, err := app.NewApp(cfg)
if err != nil {
log.Fatalf("create app: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx, cancel := context.WithCancel(context.Background())
// handle signals and restart
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
var shouldRestart bool
go func() {
select {
case <-sigCh:
fmt.Println("signal received, shutting down")
cancel()
a.Shutdown()
case <-a.RestartChan():
fmt.Println("restart requested, reloading configuration")
shouldRestart = true
cancel()
a.Shutdown()
}
}()
if err := a.Run(ctx); err != nil {
log.Fatalf("app run: %v", err)
}
// handle signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("signal received, shutting down")
cancel()
a.Shutdown()
}()
if err := a.Run(ctx); err != nil {
log.Fatalf("app run: %v", err)
if !shouldRestart {
break
}
fmt.Println("restarting honeypot...")
time.Sleep(1 * time.Second) // Brief pause before restart
}
fmt.Println("honeypot stopped")