Files
flod/main.go
jbergner 2eea551964
All checks were successful
release-tag / release-image (push) Successful in 1m49s
Check-Gui added
2025-11-12 03:33:16 +01:00

1169 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9"
)
var (
// Requests & Responses & Inflight & Duration
reqTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_requests_total",
Help: "Total HTTP requests by handler",
},
[]string{"handler"},
)
respTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_http_responses_total",
Help: "HTTP responses by handler and code",
},
[]string{"handler", "code"},
)
inflight = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_requests_inflight",
Help: "Inflight HTTP requests",
},
)
reqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ipcheck_request_duration_seconds",
Help: "Request duration seconds",
// Wähle Buckets ähnlich deinem manuellen Histogramm
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
[]string{"handler"},
)
// Importer
importCycles = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ipcheck_import_cycles_total",
Help: "Completed import cycles",
},
)
importLastSuccess = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_import_last_success_timestamp_seconds",
Help: "Last successful import Unix time",
},
)
importErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_import_errors_total",
Help: "Import errors by category",
},
[]string{"category"},
)
importDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ipcheck_import_duration_seconds",
Help: "Import duration by category",
Buckets: []float64{0.5, 1, 2, 5, 10, 30, 60, 120, 300},
},
[]string{"category"},
)
// Bereits vorhanden: blocklistHashSizes (GaugeVec)
catalogCategories = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_catalog_categories_total",
Help: "Number of categories in catalog",
},
)
// Honeypot-Teile hast du im zweiten Projekt nicht → weglassen oder später ergänzen
whitelistTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_whitelist_total",
Help: "Whitelisted IPs",
},
)
traefikBlocks = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ipcheck_traefik_blocks_total",
Help: "Traefik blocks due to matches",
},
)
downloads = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_downloads_total",
Help: "Downloads served by category",
},
[]string{"category"},
)
manualBlacklistSize = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_manual_blacklist_size",
Help: "Manual blacklist size",
},
)
)
func init() {
prometheus.MustRegister(
reqTotal, respTotal, inflight, reqDuration,
importCycles, importLastSuccess, importErrors, importDuration,
blocklistHashSizes, catalogCategories, whitelistTotal,
traefikBlocks, downloads, manualBlacklistSize,
)
// Deine existierenden Counter:
// checkRequests, checkBlocked, checkWhitelist sind okay können bleiben.
}
type statusRecorder struct {
http.ResponseWriter
code int
}
func (w *statusRecorder) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
func instrumentHandler(name string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inflight.Inc()
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, code: 200}
reqTotal.WithLabelValues(name).Inc()
next.ServeHTTP(rec, r)
inflight.Dec()
reqDuration.WithLabelValues(name).Observe(time.Since(start).Seconds())
respTotal.WithLabelValues(name, fmt.Sprintf("%d", rec.code)).Inc()
})
}
func instrumentFunc(name string, fn http.HandlerFunc) http.Handler {
return instrumentHandler(name, http.HandlerFunc(fn))
}
// --------------------------------------------------
//
// --------------------------------------------------
// Redis + Context
var ctx = context.Background()
// ──────────────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────────────
// ExportListJSON schreibt die Map als prettified JSONDatei.
func ExportListJSON(path string, m map[string]string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(m)
}
// ImportListJSON liest eine JSONDatei und gibt map[string]string zurück.
func ImportListJSON(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var m map[string]string
if err := json.NewDecoder(f).Decode(&m); err != nil {
return nil, err
}
return m, nil
}
// URLs der Blocklisten
var blocklistURLs = map[string]string{
"bitwire": "https://raw.githubusercontent.com/bitwire-it/ipblocklist/refs/heads/main/ip-list.txt",
}
// Präfix-Cache
type prefixCacheEntry struct {
prefixes []netip.Prefix
expireAt time.Time
}
var (
prefixCache = map[string]prefixCacheEntry{}
prefixCacheMu sync.RWMutex
)
// Prometheus Metriken
var (
checkBlocked = prometheus.NewCounter(prometheus.CounterOpts{
Name: "ipcheck_blocked_total",
Help: "Total blocked IPs",
})
checkWhitelist = prometheus.NewCounter(prometheus.CounterOpts{
Name: "ipcheck_whitelisted_total",
Help: "Total whitelisted IPs",
})
blocklistHashSizes = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ipcheck_blocklist_hash_size",
Help: "Number of entries in each category",
},
[]string{"category"},
)
)
func init() {
prometheus.MustRegister(checkBlocked, checkWhitelist)
}
// Main
func main() {
// Import Blocklisten
if err := importBlocklists(); err != nil {
fmt.Println("Blocklisten-Import FEHLGESCHLAGEN:", err)
return
}
// Server
http.Handle("/", instrumentFunc("gui", checkhtml))
http.Handle("/admin", instrumentFunc("admin", handleGUI))
http.Handle("/download/", instrumentFunc("download", handleDownload))
http.Handle("/whitelist", instrumentFunc("whitelist", handleWhitelist))
http.Handle("/check/", instrumentFunc("check", handleCheck))
http.Handle("/traefik", instrumentFunc("traefik", handleTraefik))
http.Handle("/metrics", promhttp.Handler())
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
updateBlocklistMetrics()
<-ticker.C
}
}()
fmt.Println("Server läuft auf :8080")
http.ListenAndServe(":8080", nil)
}
func clientIPFromHeaders(r *http.Request) (netip.Addr, error) {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
s := strings.TrimSpace(parts[0])
if a, err := netip.ParseAddr(s); err == nil {
return a.Unmap(), nil
}
}
if xr := r.Header.Get("X-Real-Ip"); xr != "" {
if a, err := netip.ParseAddr(strings.TrimSpace(xr)); err == nil {
return a.Unmap(), nil
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
if a, err := netip.ParseAddr(host); err == nil {
return a.Unmap(), nil
}
}
return netip.Addr{}, fmt.Errorf("cannot determine client ip")
}
func updateBlocklistMetrics() {
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
// Blocklist-Hash-Größen pro Kategorie
for cat := range blocklistURLs {
key := "bl:" + cat
count, err := rdb.HLen(ctx, key).Result()
if err != nil {
fmt.Printf("❌ Redis HLen Error for %s: %v\n", key, err)
continue
}
blocklistHashSizes.WithLabelValues(cat).Set(float64(count))
}
// Whitelist gesamt (wenn als Keys "wl:<ip>" gespeichert)
if n, err := rdb.Keys(ctx, "wl:*").Result(); err == nil {
whitelistTotal.Set(float64(len(n)))
}
// Manuelle Blacklist, falls vorhanden
if n, err := rdb.HLen(ctx, "bl:manual").Result(); err == nil {
manualBlacklistSize.Set(float64(n))
}
}
type target struct {
Name, URL string
}
func fetchAndSave(client *http.Client, t target, outDir string) error {
fileName := filepath.Base(t.URL)
if fileName == "" {
fileName = strings.ReplaceAll(strings.ToLower(strings.ReplaceAll(t.Name, " ", "_")), "..", "")
}
dst := filepath.Join(outDir, fileName)
log.Printf("Downloading %-40s → %s", t.Name, dst)
resp, err := client.Get(t.URL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad HTTP status: %s", resp.Status)
}
tmp := dst + ".part"
f, err := os.Create(tmp)
if err != nil {
return err
}
if _, err := io.Copy(f, resp.Body); err != nil {
f.Close()
os.Remove(tmp)
return err
}
f.Close()
return os.Rename(tmp, dst)
}
// Import-Logik
func importBlocklists() error {
startAll := time.Now()
importCycles.Inc()
client := &http.Client{Timeout: 60 * time.Second}
t := target{Name: "Catalog", URL: os.Getenv("FLOD_IMPORT_URL")}
if err := os.MkdirAll("/lists/", 0o755); err != nil {
fmt.Println("creating output dir", err)
}
if err := fetchAndSave(client, t, "/lists/"); err != nil {
log.Printf("ERROR %s → %v", t.URL, err)
}
fileName := filepath.Base(t.URL)
if fileName == "" {
fileName = strings.ReplaceAll(strings.ToLower(strings.ReplaceAll(t.Name, " ", "_")), "..", "")
}
blocklistURLs, _ = ImportListJSON("/lists/" + fileName)
catalogCategories.Set(float64(len(blocklistURLs)))
var wg sync.WaitGroup
errCh := make(chan error, len(blocklistURLs))
for cat, url := range blocklistURLs {
wg.Add(1)
go func(c, u string) {
defer wg.Done()
start := time.Now()
if err := importCategory(c, u); err != nil {
importErrors.WithLabelValues(c).Inc()
errCh <- fmt.Errorf("%s: %v", c, err)
}
importDuration.WithLabelValues(c).Observe(time.Since(start).Seconds())
}(cat, url)
}
wg.Wait()
close(errCh)
// Erfolgstimestamp nur setzen, wenn keine Fehler:
if len(errCh) == 0 {
importLastSuccess.Set(float64(time.Now().Unix()))
}
_ = startAll // (falls du Gesamtzeit noch extra messen willst)
for err := range errCh {
fmt.Println("❌", err)
}
if len(errCh) > 0 {
return fmt.Errorf("Blocklisten-Import teilweise fehlgeschlagen")
}
fmt.Println("✅ Blocklisten-Import abgeschlossen")
fmt.Println(blocklistURLs)
blocklistURLs["flodpod"] = "null"
return nil
}
func importCategory(cat, url string) error {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
fmt.Printf("⬇️ Lade %s (%s)\n", cat, url)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("HTTP-Fehler: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
scanner := bufio.NewScanner(resp.Body)
pipe := rdb.Pipeline()
count, batchCount := 0, 0
const batchSize = 500
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
prefix, valid := normalizePrefix(line)
if !valid {
fmt.Printf("⚠️ Ungültig %s: %s\n", cat, line)
continue
}
pipe.HSet(ctx, "bl:"+cat, prefix, 1)
count++
batchCount++
if batchCount >= batchSize {
if _, err := pipe.Exec(ctx); err != nil {
return fmt.Errorf("Redis-Fehler: %v", err)
}
batchCount = 0
}
if count%1000 == 0 {
fmt.Printf("📈 [%s] %d Einträge\n", cat, count)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("lesefehler: %v", err)
}
if batchCount > 0 {
if _, err := pipe.Exec(ctx); err != nil {
return fmt.Errorf("Redis-Fehler final: %v", err)
}
}
fmt.Printf("✅ [%s] %d Einträge importiert\n", cat, count)
return nil
}
func normalizePrefix(s string) (string, bool) {
if !strings.Contains(s, "/") {
ip := net.ParseIP(s)
if ip == nil {
return "", false
}
if ip.To4() != nil {
s += "/32"
} else {
s += "/128"
}
}
s = strings.TrimSpace(s)
_, err := netip.ParsePrefix(s)
return s, err == nil
}
func handleWhitelist(w http.ResponseWriter, r *http.Request) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
IP string `json:"ip"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
addr, err := netip.ParseAddr(body.IP)
if err != nil {
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
key := "wl:" + addr.String()
if err := rdb.Set(ctx, key, "1", 0).Err(); err != nil {
http.Error(w, "redis error", http.StatusInternalServerError)
return
}
// Optional: Cache leeren für die IP
prefixCacheMu.Lock()
defer prefixCacheMu.Unlock()
// Kein spezifischer IP-Cache in deinem Design, aber hier könnte man Cache invalidieren falls nötig
writeJSON(w, map[string]string{
"status": "whitelisted",
"ip": addr.String(),
})
}
// Check-Handler
func handleCheck(w http.ResponseWriter, r *http.Request) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
ipStr := strings.TrimPrefix(r.URL.Path, "/check/")
ip, err := netip.ParseAddr(ipStr)
if err != nil {
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
var cats []string
for a := range blocklistURLs {
cats = append(cats, a)
}
//cats := []string{"firehol", "bitwire", "RU", "CN"}
matches, err := checkIP(ip, cats)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
if len(matches) > 0 {
checkBlocked.Inc()
} else {
wl, _ := rdb.Exists(ctx, "wl:"+ip.String()).Result()
if wl > 0 {
checkWhitelist.Inc()
}
}
writeJSON(w, map[string]any{
"ip": ip.String(),
"blocked": len(matches) > 0,
"categories": matches,
})
}
// Check-Handler
func handleTraefik(w http.ResponseWriter, r *http.Request) {
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
ip, err := clientIPFromHeaders(r)
if err != nil {
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
// Kategorien dynamisch aus blocklistURLs
cats := make([]string, 0, len(blocklistURLs))
for c := range blocklistURLs {
cats = append(cats, c)
}
matches, err := checkIP(ip, cats)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
// Whitelist check (wie gehabt)
if len(matches) == 0 {
wl, _ := rdb.Exists(ctx, "wl:"+ip.String()).Result()
if wl > 0 {
checkWhitelist.Inc()
}
}
if len(matches) > 0 {
checkBlocked.Inc()
traefikBlocks.Inc()
errorhtml(w, r)
//http.Error(w, "blocked", http.StatusTooManyRequests)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}
// Check-Logik
func checkIP(ip netip.Addr, cats []string) ([]string, error) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
wl, err := rdb.Exists(ctx, "wl:"+ip.String()).Result()
if err != nil {
return nil, err
}
if wl > 0 {
return []string{}, nil
}
matches := []string{}
for _, cat := range cats {
prefixes, err := loadCategoryPrefixes(cat)
if err != nil {
return nil, err
}
for _, pfx := range prefixes {
if pfx.Contains(ip) {
fmt.Printf("💡 MATCH: %s in %s (%s)\n", ip, cat, pfx)
matches = append(matches, cat)
break
}
}
}
return matches, nil
}
func loadCategoryPrefixes(cat string) ([]netip.Prefix, error) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
prefixCacheMu.Lock()
defer prefixCacheMu.Unlock()
entry, ok := prefixCache[cat]
if ok && time.Now().Before(entry.expireAt) {
return entry.prefixes, nil
}
keys, err := rdb.HKeys(ctx, "bl:"+cat).Result()
if err != nil {
return nil, err
}
var prefixes []netip.Prefix
for _, k := range keys {
k = strings.TrimSpace(k)
pfx, err := netip.ParsePrefix(k)
if err == nil {
prefixes = append(prefixes, pfx)
} else {
fmt.Printf("⚠️ Ungültiger Redis-Prefix %s: %s\n", cat, k)
}
}
prefixCache[cat] = prefixCacheEntry{
prefixes: prefixes,
expireAt: time.Now().Add(10 * time.Minute),
//Hier geändert von 1 * time.Second
}
return prefixes, nil
}
// JSON-Helfer
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func handleDownload(w http.ResponseWriter, r *http.Request) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
cat := strings.TrimPrefix(r.URL.Path, "/download/")
if cat == "" {
http.Error(w, "category missing", http.StatusBadRequest)
return
}
// Prüfen, ob Kategorie existiert
if _, ok := blocklistURLs[cat]; !ok {
http.Error(w, "unknown category", http.StatusNotFound)
return
}
// Alle Einträge holen
keys, err := rdb.HKeys(ctx, "bl:"+cat).Result()
if err != nil {
http.Error(w, "redis error", http.StatusInternalServerError)
return
}
// Header für Download setzen
downloads.WithLabelValues(cat).Inc()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.txt\"", cat))
// Zeilenweise schreiben
for _, k := range keys {
_, _ = fmt.Fprintln(w, k)
}
}
func handleGUI(w http.ResponseWriter, r *http.Request) {
html := `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>IP Checker GUI</title>
<style>
body { font-family: sans-serif; max-width: 1000px; margin: auto; padding: 2em; background: #f9fafb; }
h1 { font-size: 1.5em; margin-bottom: 1em; }
input, button { padding: 0.7em; margin: 0.3em 0; width: 100%; border-radius: 0.4em; border: 1px solid #ccc; box-sizing: border-box; }
button { background: #2563eb; color: white; border: none; cursor: pointer; }
button:hover { background: #1d4ed8; }
#result, #metrics, #history { background: white; border: 1px solid #ddd; padding: 1em; border-radius: 0.4em; margin-top: 1em; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>IP Checker + Whitelist GUI</h1>
<input id="ipInput" type="text" placeholder="Enter IP-Address...">
<button onclick="checkIP()">Check IP</button>
<button onclick="whitelistIP()">Whitelist IP</button>
<h2>Ergebnis</h2>
<div id="result">No Request</div>
<h2>Check History</h2>
<div id="history">No history</div>
<h2>Prometheus Metrics</h2>
<div id="metrics">Loading...</div>
<script>
async function checkIP() {
const ip = document.getElementById('ipInput').value.trim();
if (!ip) { alert("Please enter IP!"); return; }
const res = await fetch('/check/' + ip);
const data = await res.json();
document.getElementById('result').innerText = JSON.stringify(data, null, 2);
addHistory(ip, data);
}
async function whitelistIP() {
const ip = document.getElementById('ipInput').value.trim();
if (!ip) { alert("Please enter IP!"); return; }
const res = await fetch('/whitelist', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip})
});
const data = await res.json();
document.getElementById('result').innerText = JSON.stringify(data, null, 2);
addHistory(ip, data);
}
function addHistory(ip, data) {
let history = JSON.parse(localStorage.getItem('ipHistory') || '[]');
history.unshift({ip, data, ts: new Date().toLocaleString()});
if (history.length > 10) history = history.slice(0, 10);
localStorage.setItem('ipHistory', JSON.stringify(history));
renderHistory();
}
function renderHistory() {
let history = JSON.parse(localStorage.getItem('ipHistory') || '[]');
if (history.length === 0) {
document.getElementById('history').innerText = 'Nothing checked yet';
return;
}
document.getElementById('history').innerText = history.map(e =>
e.ts + ": " + e.ip + " → blocked=" + (e.data.blocked ? "yes" : "no") +
(e.data.categories ? " [" + e.data.categories.join(", ") + "]" : "")
).join("\n");
}
async function loadMetrics() {
const res = await fetch('/metrics');
const text = await res.text();
const lines = text.split('\n').filter(l => l.includes('ipcheck_'));
document.getElementById('metrics').innerText = lines.join('\n') || 'No Data';
}
renderHistory();
setInterval(loadMetrics, 3000);
loadMetrics();
</script>
</body>
</html>
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(html))
}
func checkhtml(w http.ResponseWriter, r *http.Request) {
html := `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FLODP IP Check</title>
<style>
:root{
--bg:#f6f7f9;--text:#1f2937;--muted:#6b7280;--card:#ffffff;
--success:#22c55e;--danger:#ef4444;--accent:#2563eb;--border:#e5e7eb;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial}
.wrap{max-width:980px;margin:0 auto;padding:40px 16px 64px}
header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
h1{font-size:clamp(24px,4vw,38px);font-weight:700;margin:0}
.pill{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:600;font-size:14px;color:#111}
.pill small{font-weight:500;color:var(--muted)}
p.lead{margin:12px 0 24px;color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;margin-top:16px}
.node{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:12px;box-shadow:0 6px 18px rgba(0,0,0,.04)}
.node h3{margin:0 0 4px;font-size:16px}
.sub{margin:0;color:var(--muted);font-size:14px}
.status-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:700;width:max-content}
.status-badge.ok{color:var(--success);border-color:#bbf7d0;background:#f0fdf4}
.status-badge.err{color:var(--danger);border-color:#fecaca;background:#fef2f2}
.row{display:flex;gap:12px;flex-wrap:wrap}
.field{display:flex;flex-direction:column;gap:6px;flex:1;min-width:220px}
label{font-weight:600}
input[type="text"]{
padding:12px;border-radius:10px;border:1px solid var(--border);outline:none;background:#fff;
}
input[type="text"]:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,.15)}
.btn{border:1px solid var(--border);background:#fff;font-weight:600;padding:10px 14px;border-radius:10px;cursor:pointer}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn:disabled{opacity:.6;cursor:not-allowed}
.muted{color:var(--muted)}
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:pre-wrap;background:#f8fafc;border:1px solid var(--border);padding:12px;border-radius:10px}
.chip{display:inline-block;margin:4px 6px 0 0;padding:4px 8px;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#3730a3;font-weight:600;font-size:12px}
footer{margin-top:40px;color:var(--muted);font-size:13px;text-align:center}
.hint{font-size:13px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>IP Check</h1>
<span class="pill">FLODP <small>Security Utility</small></span>
</header>
<p class="lead">Prüfe schnell, ob eine IP in den Blocklisten gelistet ist. Die Abfrage nutzt den Endpunkt <code>/check/&lt;ip&gt;</code>.</p>
<section class="grid" aria-label="IP-Check">
<!-- Formular -->
<article class="node">
<h3>Anfrage</h3>
<p class="sub">Sende eine Abfrage an <code>/check/&lt;ip&gt;</code></p>
<form id="checkForm" class="row" novalidate>
<div class="field">
<label for="ip">IP-Adresse</label>
<input id="ip" name="ip" type="text" placeholder="z. B. 203.0.113.42 oder 2001:db8::1" autocomplete="off" required>
<small class="hint">IPv4 oder IPv6. Es erfolgt eine leichte Client-Validierung.</small>
</div>
<div class="row" style="align-items:flex-end">
<button id="btnCheck" class="btn primary" type="submit">Check ausführen</button>
<button id="btnClear" class="btn" type="button">Zurücksetzen</button>
</div>
</form>
</article>
<!-- Ergebnis -->
<article class="node" id="resultCard" aria-live="polite">
<h3>Ergebnis</h3>
<div id="statusBadge" class="status-badge" style="display:none"></div>
<div id="summary" class="muted">Noch keine Abfrage durchgeführt.</div>
<div id="catsWrap" style="display:none">
<strong>Kategorien:</strong>
<div id="cats"></div>
</div>
<details id="rawWrap" style="margin-top:8px; display:none">
<summary><strong>Rohdaten (Response JSON)</strong></summary>
<pre id="raw" class="code"></pre>
</details>
</article>
</section>
<footer>
<span>© First-Line-Of-Defense-Project</span>
</footer>
</div>
<script>
const form = document.getElementById('checkForm');
const ipInput = document.getElementById('ip');
const btnCheck = document.getElementById('btnCheck');
const btnClear = document.getElementById('btnClear');
const statusBadge = document.getElementById('statusBadge');
const summary = document.getElementById('summary');
const catsWrap = document.getElementById('catsWrap');
const cats = document.getElementById('cats');
const rawWrap = document.getElementById('rawWrap');
const raw = document.getElementById('raw');
// Simple IPv4/IPv6 Check (nicht perfekt, aber hilfreich)
function looksLikeIP(value){
const v = value.trim();
const ipv4 = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
const ipv6 = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i; // sehr tolerant
return ipv4.test(v) || ipv6.test(v);
}
function setLoading(loading){
btnCheck.disabled = loading;
btnCheck.textContent = loading ? 'Wird geprüft…' : 'Check ausführen';
ipInput.disabled = loading;
}
function setStatus(ok, text){
statusBadge.style.display = 'inline-flex';
statusBadge.className = 'status-badge ' + (ok ? 'ok' : 'err');
statusBadge.textContent = ok ? 'OK • not listed' : 'BLOCKED • listed';
summary.textContent = text;
}
function resetUI(){
statusBadge.style.display = 'none';
statusBadge.className = 'status-badge';
summary.textContent = 'Noch keine Abfrage durchgeführt.';
catsWrap.style.display = 'none';
cats.innerHTML = '';
rawWrap.style.display = 'none';
raw.textContent = '';
}
btnClear.addEventListener('click', () => {
form.reset();
resetUI();
ipInput.focus();
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const ip = ipInput.value.trim();
if(!looksLikeIP(ip)){
ipInput.focus();
ipInput.select();
summary.textContent = 'Bitte eine gültige IPv4- oder IPv6-Adresse eingeben.';
statusBadge.style.display = 'inline-flex';
statusBadge.className = 'status-badge err';
statusBadge.textContent = 'Ungültige IP';
catsWrap.style.display = 'none';
rawWrap.style.display = 'none';
return;
}
setLoading(true);
try{
const res = await fetch('/check/' + encodeURIComponent(ip));
const data = await res.json();
// Erwartete Struktur: { ip: "...", blocked: bool, categories: [] }
const ok = data && data.blocked === false;
setStatus(ok, ok
? 'Die IP ' + data.ip + ' ist nicht gelistet.'
: 'Die IP ' + data.ip + ' ist gelistet.');
// Kategorien
const list = Array.isArray(data.categories) ? data.categories : [];
if(!ok && list.length > 0){
catsWrap.style.display = 'block';
cats.innerHTML = list.map(function(c){ return '<span class="chip">' + c + '</span>'; }).join('');
}else{
catsWrap.style.display = 'none';
cats.innerHTML = '';
}
// Rohdaten anzeigen
rawWrap.style.display = 'block';
raw.textContent = JSON.stringify(data, null, 2);
}catch(err){
setStatus(false, 'Fehler bei der Abfrage. Details siehe Konsole.');
console.error(err);
rawWrap.style.display = 'none';
}finally{
setLoading(false);
}
});
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(html))
}
func errorhtml(w http.ResponseWriter, r *http.Request) {
html := `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Blocked by the First-Line-Of-Defense-Project</title>
<style>
:root{
--bg:#f6f7f9;--text:#1f2937;--muted:#6b7280;--card:#ffffff;
--success:#22c55e;--danger:#ef4444;--accent:#2563eb;--border:#e5e7eb;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial}
.wrap{max-width:980px;margin:0 auto;padding:40px 16px 64px}
header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
h1{font-size:clamp(24px,4vw,38px);font-weight:700;margin:0}
.pill{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:600;font-size:14px;color:#111}
.pill small{font-weight:500;color:var(--muted)}
p.lead{margin:12px 0 24px;color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;margin-top:16px}
.node{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px;display:flex;flex-direction:column;align-items:center;gap:10px;box-shadow:0 6px 18px rgba(0,0,0,.04)}
.icon{position:relative;width:104px;height:80px;display:grid;place-items:center}
.status{position:absolute;right:-8px;bottom:-8px;width:30px;height:30px;border-radius:999px;display:grid;place-items:center;color:#fff;font-weight:800;font-size:14px}
.status.ok{background:var(--success)}
.status.err{background:var(--danger)}
.node h3{margin:6px 0 0;font-size:16px}
.node .sub{margin:0;color:var(--muted);font-size:14px}
.node .state{margin:4px 0 0;font-weight:700}
.state.ok{color:var(--success)}
.state.err{color:var(--danger)}
.actions{margin-top:28px;display:flex;gap:12px;flex-wrap:wrap}
.btn{border:1px solid var(--border);background:#fff;font-weight:600;padding:10px 14px;border-radius:10px;cursor:pointer}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.meta{margin-top:24px;color:var(--muted);font-size:13px}
footer{margin-top:40px;color:var(--muted);font-size:13px}
/* Simple, friendly SVG look */
svg{display:block}
.dim{fill:#e5e7eb}
.stroke{stroke:#9ca3af}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>You have been blocked by the First-Line-Of-Defense-Project</h1>
<span class="pill">ERROR&nbsp;403 <small>Forbidden</small></span>
</header>
<p class="lead">
Your connection attempt to the target server was blocked by the First-Line-Of-Defense-Project. Your IP address is listed on at least one blacklist.
</p>
<section class="grid" aria-label="Diagnostic chain">
<article class="node" aria-label="Browser Status">
<div class="icon" aria-hidden="true">
<svg width="88" height="62" viewBox="0 0 88 62" xmlns="http://www.w3.org/2000/svg" role="img">
<rect x="1" y="6" width="86" height="55" rx="8" fill="#fff" stroke="#d1d5db"/>
<rect x="1" y="1" width="86" height="14" rx="8" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="10" cy="8" r="2.5" fill="#ef4444"/>
<circle cx="18" cy="8" r="2.5" fill="#f59e0b"/>
<circle cx="26" cy="8" r="2.5" fill="#22c55e"/>
</svg>
<div class="status ok" title="Functional">✓</div>
</div>
<h3>You</h3>
<p class="sub">Browser</p>
<p class="state ok">Functional</p>
</article>
<!-- Edge / Proxy -->
<article class="node" aria-label="FLODP Status">
<div class="icon" aria-hidden="true">
<svg width="96" height="64" viewBox="0 0 96 64" xmlns="http://www.w3.org/2000/svg" role="img">
<path d="M33 44h32a14 14 0 0 0 0-28 18 18 0 0 0-34-5 16 16 0 0 0-4 31z" fill="#e5e7eb" stroke="#d1d5db"/>
</svg>
<div class="status err" title="Blocked">✕</div>
</div>
<h3>FLODP-SERVICE</h3>
<p class="sub">Security-Gateway</p>
<p class="state err">Blocked your request</p>
</article>
<!-- Host / Origin -->
<article class="node" aria-label="Origin/Host Status">
<div class="icon" aria-hidden="true">
<svg width="88" height="62" viewBox="0 0 88 62" xmlns="http://www.w3.org/2000/svg" role="img">
<rect x="6" y="10" width="76" height="18" rx="4" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="16" cy="19" r="3" fill="#9ca3af"/>
<rect x="6" y="34" width="76" height="18" rx="4" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="16" cy="43" r="3" fill="#9ca3af"/>
</svg>
<div class="status ok" title="Functional">✓</div>
</div>
<h3>Host</h3>
<p class="sub">Origin-Server</p>
<p class="state ok">Functional</p>
</article>
</section>
<div class="actions">
<button class="btn primary" onclick="location.reload()">Try again</button>
<button class="btn" onclick="document.getElementById('details').toggleAttribute('open')">Show details</button>
</div>
<details id="details" class="meta">
<summary><strong>Technical details</strong></summary>
<ul>
<li>Error: <strong>403</strong> - Your IP address is listed on at least one blacklist. The service's security system has therefore rejected your connection.</li>
<li>Time: <span id="now">-</span></li>
</ul>
<p>Tips: Check if your system (browser, API, or similar) has a high connection frequency and has been blocked on other systems protected by FLODP.</p>
</details>
<footer>
<span>If the problem persists, contact the website operator.</span>
</footer>
</div>
<script>
(function(){
const now = new Date()
document.getElementById('now').textContent = now.toLocaleString()
})()
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(html))
}