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

\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.