diff --git a/cmd/server/main.go b/cmd/server/main.go index 959b461..8712a7e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -384,18 +384,11 @@ func createAndMountISO(size, mountpoint, charset string) error { var dialer = &net.Dialer{ DualStack: true, Timeout: 5 * time.Second, - KeepAlive: 30 * time.Second, // Added keep-alive for better network change handling } var dualStackClient = &http.Client{ Transport: &http.Transport{ - DialContext: dialer.DialContext, - ForceAttemptHTTP2: true, // Enforce HTTP/2 - IdleConnTimeout: 90 * time.Second, // Longer idle connections - DisableKeepAlives: false, // Ensure keep-alives are enabled - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - // ...existing code... + DialContext: dialer.DialContext, }, } @@ -542,6 +535,10 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + if conf.Server.NetworkEvents { + go monitorNetwork(ctx) + go handleNetworkEvents(ctx) + } go updateSystemMetrics(ctx) if conf.ClamAV.ClamAVEnabled { @@ -637,6 +634,9 @@ func main() { log.Fatalf("Server failed: %v", err) } } else { + if conf.Server.ListenPort == "0.0.0.0" { + log.Info("Binding to 0.0.0.0. Any net/http logs you see are normal for this universal address.") + } if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed: %v", err) } @@ -733,7 +733,7 @@ uploadqueuesize = 50 # Add file-specific configurations here [build] -version = "2.8-Stable" +version = "2.7-Stable" `) } @@ -1700,7 +1700,26 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { log.WithFields(logrus.Fields{"method": r.Method, "url": r.URL.String(), "remote": clientIP}).Info("Incoming request") + // Log the requested URL for debugging + log.Infof("handleRequest: Received URL path: %s", r.URL.String()) + p := r.URL.Path + fileStorePath := strings.TrimPrefix(p, "/") + if fileStorePath == "" || fileStorePath == "/" { + log.WithField("path", fileStorePath).Warn("No file specified in URL") + // Updated to return 404 with a clear message instead of forbidden. + http.Error(w, "File not specified in URL. Please include the file path after the host.", http.StatusNotFound) + flushLogMessages() + return + } + // NEW: Compute absolute file path from storage path and fileStorePath. + absFilename, err := sanitizeFilePath(conf.Server.StoragePath, fileStorePath) + if err != nil { + log.WithError(err).Warn("Invalid file path") + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } + a, err := url.ParseQuery(r.URL.RawQuery) if err != nil { log.Warn("Failed to parse query parameters") @@ -1708,26 +1727,6 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } - fileStorePath := strings.TrimPrefix(p, "/") - if fileStorePath == "" || fileStorePath == "/" { - log.WithFields(logrus.Fields{ - "event": "AccessAttempt", - "severity": "warning", - }).Warn("Access to root directory is forbidden") - http.Error(w, "Forbidden", http.StatusForbidden) - flushLogMessages() - return - } else if fileStorePath[0] == '/' { - fileStorePath = fileStorePath[1:] - } - - absFilename, err := sanitizeFilePath(conf.Server.StoragePath, fileStorePath) - if err != nil { - log.WithFields(logrus.Fields{"file": fileStorePath, "error": err}).Warn("Invalid file path") - http.Error(w, "Invalid file path", http.StatusBadRequest) - return - } - switch r.Method { case http.MethodPut: handleUpload(w, r, absFilename, fileStorePath, a) @@ -1745,14 +1744,6 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { // handleUpload handles PUT requests for file uploads func handleUpload(w http.ResponseWriter, r *http.Request, absFilename, fileStorePath string, a url.Values) { - clientIP := getOriginalClientIP(r) - parsedIP := net.ParseIP(clientIP) - if parsedIP == nil { - log.Warnf("Invalid client IP address: %s", clientIP) - } else { - log.Infof("Handling upload from IP: %s (%s)", parsedIP.String(), detectIPVersion(parsedIP.String())) - } - log.Infof("Using storage path: %s", conf.Server.StoragePath) // HMAC validation @@ -1860,7 +1851,6 @@ func handleUpload(w http.ResponseWriter, r *http.Request, absFilename, fileStore } // Respond with 201 Created immediately - w.Header().Set("Content-Type", "text/plain") // Ensure correct interpretation w.WriteHeader(http.StatusCreated) if f, ok := w.(http.Flusher); ok { f.Flush() @@ -1965,14 +1955,7 @@ func handleDownload(w http.ResponseWriter, r *http.Request, absFilename, fileSto } else { startTime := time.Now() log.Infof("Initiating download for file: %s", absFilename) - f, err := os.Open(absFilename) - if err != nil { - log.Errorf("Couldn't open file: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - defer f.Close() - http.ServeContent(w, r, filepath.Base(absFilename), fileInfo.ModTime(), f) + http.ServeFile(w, r, absFilename) downloadDuration.Observe(time.Since(startTime).Seconds()) downloadSizeBytes.Observe(float64(fileInfo.Size())) downloadsTotal.Inc() @@ -2172,6 +2155,73 @@ func getFileInfo(absFilename string) (os.FileInfo, error) { return fileInfo, nil } +func monitorNetwork(ctx context.Context) { + currentIP := getCurrentIPAddress() + + for { + select { + case <-ctx.Done(): + log.Info("Stopping network monitor.") + return + case <-time.After(10 * time.Second): + newIP := getCurrentIPAddress() + if newIP != currentIP && newIP != "" { + currentIP = newIP + select { + case networkEvents <- NetworkEvent{Type: "IP_CHANGE", Details: currentIP}: + log.WithField("new_ip", currentIP).Info("Queued IP_CHANGE event") + default: + log.Warn("Network event channel full. Dropping IP_CHANGE event.") + } + } + } + } +} + +func handleNetworkEvents(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Info("Stopping network event handler.") + return + case event, ok := <-networkEvents: + if !ok { + log.Info("Network events channel closed.") + return + } + switch event.Type { + case "IP_CHANGE": + log.WithField("new_ip", event.Details).Info("Network change detected") + } + } + } +} + +func getCurrentIPAddress() string { + interfaces, err := net.Interfaces() + if err != nil { + log.WithError(err).Error("Failed to get network interfaces") + return "" + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + log.WithError(err).Errorf("Failed to get addresses for interface %s", iface.Name) + continue + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() && ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return "" +} + func setupGracefulShutdown(server *http.Server, cancel context.CancelFunc) { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) @@ -2757,23 +2807,13 @@ func detectIPVersion(ip string) string { } func getOriginalClientIP(r *http.Request) string { - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - parts := strings.Split(xff, ",") - for _, part := range parts { - ip := strings.TrimSpace(part) - if net.ParseIP(ip) != nil { - return ip - } - } + if ip := r.Header.Get("X-Forwarded-For"); ip != "" { + parts := strings.Split(ip, ",") + return strings.TrimSpace(parts[0]) } - if rip := r.Header.Get("X-Real-IP"); rip != "" { - if net.ParseIP(rip) != nil { - return strings.TrimSpace(rip) - } + if ip := r.Header.Get("X-Real-IP"); ip != "" { + return strings.TrimSpace(ip) } - host, _, err := net.SplitHostPort(r.RemoteAddr) - if err == nil && host != "" && net.ParseIP(host) != nil { - return host - } - return "" + host, _, _ := net.SplitHostPort(r.RemoteAddr) + return host } diff --git a/dashboard/dashboard.json b/dashboard/dashboard.json index 0280d74..43695e8 100644 --- a/dashboard/dashboard.json +++ b/dashboard/dashboard.json @@ -27,8 +27,8 @@ "overrides": [] }, "gridPos": { - "h": 6, - "w": 24, + "h": 7, + "w": 3, "x": 0, "y": 0 }, @@ -42,7 +42,7 @@ "content": "
\n

HMAC Dashboard

\n \"HMAC\n

\n This dashboard monitors key metrics for the \n HMAC File Server.\n

\n
\n", "mode": "html" }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "title": "HMAC Dashboard", "type": "text" }, @@ -77,8 +77,8 @@ "gridPos": { "h": 7, "w": 6, - "x": 0, - "y": 6 + "x": 3, + "y": 0 }, "id": 14, "options": { @@ -105,7 +105,7 @@ "sizing": "auto", "valueMode": "color" }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -143,8 +143,8 @@ "gridPos": { "h": 7, "w": 6, - "x": 6, - "y": 6 + "x": 9, + "y": 0 }, "id": 18, "options": { @@ -164,7 +164,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -203,9 +203,9 @@ }, "gridPos": { "h": 7, - "w": 4, - "x": 12, - "y": 6 + "w": 5, + "x": 15, + "y": 0 }, "id": 10, "options": { @@ -225,7 +225,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -262,8 +262,8 @@ "gridPos": { "h": 7, "w": 4, - "x": 16, - "y": 6 + "x": 20, + "y": 0 }, "id": 17, "options": { @@ -283,7 +283,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -297,77 +297,6 @@ "title": "HMAC GoRoutines", "type": "stat" }, - { - "datasource": { - "default": true, - "type": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 4, - "x": 20, - "y": 6 - }, - "id": 15, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.5.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "bduehd5vqv1moa" - }, - "editorMode": "code", - "expr": "hmac_file_server_upload_duration_seconds_sum + hmac_file_server_download_duration_seconds_sum", - "hide": false, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "B" - } - ], - "title": "HMAC Up/Down Duration", - "type": "stat" - }, { "datasource": { "default": true, @@ -397,7 +326,7 @@ "h": 7, "w": 5, "x": 0, - "y": 13 + "y": 7 }, "id": 11, "options": { @@ -417,7 +346,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -460,7 +389,7 @@ "h": 7, "w": 5, "x": 5, - "y": 13 + "y": 7 }, "id": 12, "options": { @@ -480,7 +409,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -493,6 +422,77 @@ "title": "HMAC Downloads", "type": "stat" }, + { + "datasource": { + "default": true, + "type": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 3, + "x": 10, + "y": 7 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "bduehd5vqv1moa" + }, + "editorMode": "code", + "expr": "hmac_file_server_upload_duration_seconds_sum + hmac_file_server_download_duration_seconds_sum", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "HMAC Up/Down Duration", + "type": "stat" + }, { "datasource": { "default": true, @@ -519,9 +519,9 @@ }, "gridPos": { "h": 7, - "w": 5, - "x": 10, - "y": 13 + "w": 3, + "x": 13, + "y": 7 }, "id": 13, "options": { @@ -541,7 +541,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -580,9 +580,9 @@ }, "gridPos": { "h": 7, - "w": 5, - "x": 15, - "y": 13 + "w": 3, + "x": 16, + "y": 7 }, "id": 2, "options": { @@ -602,7 +602,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -641,9 +641,9 @@ }, "gridPos": { "h": 7, - "w": 4, - "x": 20, - "y": 13 + "w": 5, + "x": 19, + "y": 7 }, "id": 21, "options": { @@ -663,7 +663,7 @@ "textMode": "value", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -704,7 +704,7 @@ "h": 7, "w": 3, "x": 0, - "y": 20 + "y": 14 }, "id": 19, "options": { @@ -724,7 +724,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -746,7 +746,21 @@ "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "stacking": { + "group": "A", + "mode": "none" + } }, "fieldMinMax": false, "mappings": [], @@ -762,7 +776,8 @@ "value": 80 } ] - } + }, + "unit": "files" }, "overrides": [] }, @@ -770,42 +785,37 @@ "h": 7, "w": 7, "x": 3, - "y": 20 + "y": 14 }, "id": 22, "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + "tooltip": { + "mode": "single", + "sort": "none" + } }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", "exemplar": false, - "expr": "hmac_active_connections_total", + "expr": "increase(hmac_file_server_clamav_scans_total[24h])", "format": "time_series", - "instant": false, + "instant": true, "interval": "", "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "HMAC Active Connection(s)", - "type": "stat" + "title": "HMAC ClamAV San (24h)", + "type": "histogram" }, { "datasource": { @@ -818,36 +828,17 @@ "mode": "palette-classic" }, "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 80, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, - "insertNulls": false, - "lineInterpolation": "linear", "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, "stacking": { "group": "A", "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" } }, "fieldMinMax": false, @@ -873,7 +864,7 @@ "h": 7, "w": 7, "x": 10, - "y": 20 + "y": 14 }, "id": 23, "options": { @@ -884,17 +875,16 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", "exemplar": false, - "expr": "hmac_deduplication_errors_total", + "expr": "increase(hmac_file_server_clamav_errors_total[24h])", "format": "time_series", "instant": true, "interval": "", @@ -903,8 +893,8 @@ "refId": "A" } ], - "title": "HMAC Duplication Error", - "type": "timeseries" + "title": "HMAC ClamAV SanError(s) (24h)", + "type": "histogram" }, { "datasource": { @@ -951,7 +941,7 @@ "h": 7, "w": 7, "x": 17, - "y": 20 + "y": 14 }, "id": 16, "options": { @@ -962,12 +952,11 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "11.5.2", + "pluginVersion": "11.4.0", "targets": [ { "editorMode": "code", @@ -986,20 +975,19 @@ } ], "preload": false, - "refresh": "10s", "schemaVersion": 40, "tags": [], "templating": { "list": [] }, "time": { - "from": "now-5m", + "from": "now-24h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "HMAC File Server Metrics", "uid": "de0ye5t0hzq4ge", - "version": 158, + "version": 153, "weekStart": "" -} \ No newline at end of file +} diff --git a/lib/maps/iter.go b/lib/maps/iter.go new file mode 100644 index 0000000..91b549c --- /dev/null +++ b/lib/maps/iter.go @@ -0,0 +1,69 @@ +package maps + +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +type Seq2[K comparable, V any] func(yield func(K, V) bool) +type Seq[K any] func(yield func(K) bool) + +func All[Map ~map[K]V, K comparable, V any](m Map) Seq2[K, V] { + return func(yield func(K, V) bool) { + for k, v := range m { + if !yield(k, v) { + return + } + } + } +} + +// All returns an iterator over key-value pairs from m. +// The iteration order is not specified and is not guaranteed +// to be the same from one call to the next. + +func Insert[Map ~map[K]V, K comparable, V any](m Map, seq Seq2[K, V]) { + seq(func(k K, v V) bool { + m[k] = v + return true + }) +} + +// Insert adds the key-value pairs from seq to m. +// If a key in seq already exists in m, its value will be overwritten. + +func Collect[K comparable, V any](seq Seq2[K, V]) map[K]V { + m := make(map[K]V) + Insert(m, seq) + return m +} + +// Collect collects key-value pairs from seq into a new map +// and returns it. + +func Keys[Map ~map[K]V, K comparable, V any](m Map) Seq[K] { + return func(yield func(K) bool) { + for k := range m { + if !yield(k) { + return + } + } + } +} + +// Keys returns an iterator over keys in m. +// The iteration order is not specified and is not guaranteed +// to be the same from one call to the next. + +func Values[Map ~map[K]V, K comparable, V any](m Map) Seq[V] { + return func(yield func(V) bool) { + for _, v := range m { + if !yield(v) { + return + } + } + } +} + +// Values returns an iterator over values in m. +// The iteration order is not specified and is not guaranteed +// to be the same from one call to the next.