diff --git a/internal/backer/newServer.go b/internal/backer/newServer.go index 7f48401..358e662 100644 --- a/internal/backer/newServer.go +++ b/internal/backer/newServer.go @@ -48,6 +48,23 @@ func isTLSError(err error) bool { return strings.HasPrefix(err.Error(), "tls:") } +// withStatusLogging wraps a handler to log 404 and 405 responses. +func withStatusLogging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rw := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK} + next.ServeHTTP(rw, r) + + clientIP := getClientIP(r) + + switch rw.statusCode { + case http.StatusNotFound: + log.Infof("Not Found from %s to %s", clientIP, r.RequestURI) + case http.StatusMethodNotAllowed: + log.Infof("Method Not Allowed from %s for %s %s", clientIP, r.Method, r.RequestURI) + } + }) +} + // withServerHeader wraps a handler to add Server header to all responses. func withServerHeader(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -74,6 +91,13 @@ func NewServer(configPath string) (*ServerWrapper, error) { mux := http.NewServeMux() backupHandler := func(w http.ResponseWriter, r *http.Request) { + // Only GET method is allowed. + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + + return + } + username, password, ok := r.BasicAuth() // No auth? Good bye. @@ -219,7 +243,7 @@ func NewServer(configPath string) (*ServerWrapper, error) { srv := &ServerWrapper{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", C.Address, C.Port), - Handler: withServerHeader(mux), + Handler: withServerHeader(withStatusLogging(mux)), ReadHeaderTimeout: readHeaderTimeout, WriteTimeout: time.Duration(C.BackupTimeout) * time.Minute, IdleTimeout: idleTimeout, @@ -235,7 +259,7 @@ func NewServer(configPath string) (*ServerWrapper, error) { srv := &ServerWrapper{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", C.Address, C.Port), - Handler: withServerHeader(mux), + Handler: withServerHeader(withStatusLogging(mux)), ReadHeaderTimeout: readHeaderTimeout, WriteTimeout: time.Duration(C.BackupTimeout) * time.Minute, IdleTimeout: idleTimeout, @@ -374,8 +398,20 @@ type ( ServerWrapper struct { *http.Server } + + // responseWriterWrapper wraps http.ResponseWriter to capture status code. + responseWriterWrapper struct { + http.ResponseWriter + statusCode int + } ) +// WriteHeader captures the status code for logging. +func (rw *responseWriterWrapper) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + // Serve starts the server and classifies any errors returned. // TLS-related errors are logged at debug level, other errors at warn level. func (sw *ServerWrapper) Serve() error { diff --git a/internal/backer/newServer_test.go b/internal/backer/newServer_test.go index 811c998..1b103ae 100644 --- a/internal/backer/newServer_test.go +++ b/internal/backer/newServer_test.go @@ -523,4 +523,99 @@ func TestCopyWithContextCancellation(t *testing.T) { } } +// TestStatusLogging404 tests that 404 responses are logged. +func TestStatusLogging404(t *testing.T) { + C = Config{} // Reset global config. + excludePatterns = nil + + // Create a minimal mux that returns 404 for all paths + testMux := http.NewServeMux() + testMux.HandleFunc("/archive", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + wrappedMux := withStatusLogging(testMux) + + // Use a ResponseRecorder to capture the response + rr := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/nonexistent", nil) + req.RemoteAddr = "127.0.0.1:12345" + + wrappedMux.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Errorf("Expected status 404, got %d", rr.Code) + } +} + +// TestStatusLogging405 tests that 405 responses are logged. +func TestStatusLogging405(t *testing.T) { + C = Config{} // Reset global config. + excludePatterns = nil + + // Create a minimal mux that returns 405 for wrong method + testMux := http.NewServeMux() + testMux.HandleFunc("/archive", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + }) + + wrappedMux := withStatusLogging(testMux) + + rr := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/archive", nil) + req.RemoteAddr = "127.0.0.1:12345" + + wrappedMux.ServeHTTP(rr, req) + + if rr.Code != http.StatusMethodNotAllowed { + t.Errorf("Expected status 405, got %d", rr.Code) + } +} + +// TestMethodNotAllowedHandler tests that non-GET methods return 405. +func TestMethodNotAllowedHandler(t *testing.T) { + C = Config{ + Address: "0.0.0.0", + Port: 8086, + Location: "/archive", + User: "testuser", + Password: "testpass", + Directories: []string{"../../test_data/test1/foo"}, + } + excludePatterns = nil + + // Create test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != C.User || password != C.Password { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + + return + } + + if r.Method != http.MethodGet { + w.Header().Set("Server", "backer") + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + + return + } + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Test POST method + req, _ := http.NewRequest("POST", server.URL+"/archive", nil) + req.SetBasicAuth("testuser", "testpass") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("Expected status 405, got %d", resp.StatusCode) + } +} + /* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */