From e5981442bbef6279a2b952fc09d2b128832e60a6 Mon Sep 17 00:00:00 2001 From: Vladimir Kuznichenkov Date: Sun, 15 Dec 2024 11:53:20 +0200 Subject: [PATCH 1/2] Add json response format to clamav-http We want to make sure that developers can easily operate with antivirus. For that we need to make response easily parsable. With extended JSON output I want to make sure that we can distinguish between files encrypted with password and infected files. --- clamav-http/server/v1alpha/docs/scan.md | 81 ++++++++++++++++++-- clamav-http/server/v1alpha/scan_handler.go | 87 +++++++++++++++------- 2 files changed, 137 insertions(+), 31 deletions(-) diff --git a/clamav-http/server/v1alpha/docs/scan.md b/clamav-http/server/v1alpha/docs/scan.md index 6f37bb3..59170a3 100644 --- a/clamav-http/server/v1alpha/docs/scan.md +++ b/clamav-http/server/v1alpha/docs/scan.md @@ -1,27 +1,96 @@ -# POST /v1alpha/scan +# POST /v1alpha/scan -## Example Request +## Example Requests +### Plain text response ``` curl -s -F "name=eicar" -F "file=@test/eicar.txt" http://clamav-http/v1alpha/scan ``` -## Response +### JSON response +``` +curl -s -H "Accept: application/json" -F "name=eicar" -F "file=@test/eicar.txt" http://clamav-http/v1alpha/scan +``` + +## Responses ### File is infected +Plain text: +``` +HTTP/1.1 403 Forbidden +AV Response : FOUND +``` + +JSON: ``` HTTP/1.1 403 Forbidden +Content-Type: application/json + +{ + "status": "FOUND", + "description": "Eicar-Test-Signature", + "raw": "stream: Eicar-Test-Signature FOUND", + "message": "FOUND" +} +``` + +### File is encrypted + +Plain text: +``` +HTTP/1.1 403 Forbidden +AV Response : FOUND +``` +JSON: +``` +HTTP/1.1 403 Forbidden +Content-Type: application/json -Everything ok : false +{ + "status": "FOUND", + "description": "Heuristics.Encrypted.Zip", + "raw": "stream: Heuristics.Encrypted.Zip FOUND", + "message": "FOUND" +} ``` ### File is clean +Plain text: ``` HTTP/1.1 200 OK +AV Response : OK +``` + +JSON: +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "status": "OK", + "description": "", + "raw": "stream: OK", + "message": "OK" +} +``` + +### Error responses +Plain text: +``` +HTTP/1.1 400 Bad Request +Invalid request: Error message here +``` -Everything ok : true -``` \ No newline at end of file +JSON: +``` +HTTP/1.1 500 Internal Server Error +Content-Type: application/json + +{ + "error": "Error message here" +} +``` diff --git a/clamav-http/server/v1alpha/scan_handler.go b/clamav-http/server/v1alpha/scan_handler.go index 5498c96..0348ec3 100644 --- a/clamav-http/server/v1alpha/scan_handler.go +++ b/clamav-http/server/v1alpha/scan_handler.go @@ -1,12 +1,35 @@ package v1alpha import ( + "encoding/json" "net/http" + "strings" "github.com/dutchcoders/go-clamd" "github.com/sirupsen/logrus" ) +type ScanResponse struct { + Status string `json:"status"` + Description string `json:"description"` + Raw string `json:"raw"` + Message string `json:"message"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} + +func writeError(w http.ResponseWriter, wantJSON bool, status int, message string) { + w.WriteHeader(status) + if wantJSON { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ErrorResponse{Error: message}) + } else { + w.Write([]byte(message)) + } +} + type ScanHandler struct { Address string Max_file_mem int64 @@ -20,63 +43,77 @@ const ( ) func (sh *ScanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - err := r.ParseMultipartForm(sh.Max_file_mem * 1024 * 1024) + wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json") + err := r.ParseMultipartForm(sh.Max_file_mem * 1024 * 1024) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) sh.Logger.Errorf("scan error %d: %s", scan_error_0, err.Error()) + writeError(w, wantJSON, http.StatusBadRequest, "Invalid request: "+err.Error()) return } files := r.MultipartForm.File["file"] - if len(files) == 0 { - w.WriteHeader(http.StatusNoContent) - w.Write([]byte("empty file\n")) + writeError(w, wantJSON, http.StatusNoContent, "empty file") return } f, err := files[0].Open() defer f.Close() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) sh.Logger.Errorf("scan error %d: %s", scan_error_1, err.Error()) + writeError(w, wantJSON, http.StatusInternalServerError, err.Error()) return } c := clamd.NewClamd(sh.Address) response, err := c.ScanStream(f, make(chan bool)) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) sh.Logger.Errorf("scan error %d: %s", scan_error_2, err.Error()) + writeError(w, wantJSON, http.StatusInternalServerError, err.Error()) return } result := <-response - if result.Status == "OK" { - w.WriteHeader(http.StatusOK) + var status int + var message string + + switch result.Status { + case "OK": + status = http.StatusOK + message = "OK" sh.Logger.Infof("Scanning %v: RES_OK", files[0].Filename) - w.Write([]byte("AV Response : OK\n")) - } else if result.Status == "FOUND" { - w.WriteHeader(http.StatusForbidden) + case "FOUND": + status = http.StatusForbidden + message = "FOUND" sh.Logger.Infof("Scanning %v: RES_FOUND", files[0].Filename) - w.Write([]byte("AV Response : FOUND\n")) - } else if result.Status == "ERROR" { - w.WriteHeader(http.StatusBadRequest) + case "ERROR": + status = http.StatusBadRequest + message = "ERROR" sh.Logger.Infof("Scanning %v: RES_ERROR", files[0].Filename) - w.Write([]byte("AV Response : ERROR\n")) - } else if result.Status == "PARSE_ERROR" { - w.WriteHeader(http.StatusPreconditionFailed) + case "PARSE_ERROR": + status = http.StatusPreconditionFailed + message = "PARSE_ERROR" sh.Logger.Infof("Scanning %v: RES_PARSE_ERROR", files[0].Filename) - w.Write([]byte("AV Response : PARSE_ERROR\n")) + default: + status = http.StatusNotImplemented + message = "NOT_IMPLEMENTED" + } + + w.WriteHeader(status) + + if wantJSON { + w.Header().Set("Content-Type", "application/json") + response := ScanResponse{ + Status: result.Status, + Description: result.Description, + Raw: result.Raw, + Message: message, + } + json.NewEncoder(w).Encode(response) } else { - w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("AV Response : " + message + "\n")) } return } From d9a0ba49c6b02a2be4de6b8cc43486769b17b1be Mon Sep 17 00:00:00 2001 From: Vladimir Kuznichenkov Date: Sun, 15 Dec 2024 11:56:06 +0200 Subject: [PATCH 2/2] Prevent memory leak by closing channels and cleaning multipart Cleaning up temporary files created by multipart form parsing should resolve growing local FS. `make(chan bool)` is created but never closed, which cause a memory leak. --- clamav-http/server/v0/scan_handler.go | 5 ++++- clamav-http/server/v0/scan_reply_handler.go | 5 ++++- clamav-http/server/v1alpha/health_handler.go | 4 +++- clamav-http/server/v1alpha/scan_handler.go | 6 +++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/clamav-http/server/v0/scan_handler.go b/clamav-http/server/v0/scan_handler.go index cee3eeb..abc50e6 100644 --- a/clamav-http/server/v0/scan_handler.go +++ b/clamav-http/server/v0/scan_handler.go @@ -40,13 +40,16 @@ func (sh *ScanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } c := clamd.NewClamd(sh.Address) - response, err := c.ScanStream(f, make(chan bool)) + abort := make(chan bool) + defer close(abort) + response, err := c.ScanStream(f, abort) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("not okay")) return } + defer r.MultipartForm.RemoveAll() result := <-response w.WriteHeader(http.StatusOK) diff --git a/clamav-http/server/v0/scan_reply_handler.go b/clamav-http/server/v0/scan_reply_handler.go index 8a84d88..e514aeb 100644 --- a/clamav-http/server/v0/scan_reply_handler.go +++ b/clamav-http/server/v0/scan_reply_handler.go @@ -40,13 +40,16 @@ func (srh *ScanReplyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } c := clamd.NewClamd(srh.Address) - response, err := c.ScanStream(f, make(chan bool)) + abort := make(chan bool) + defer close(abort) + response, err := c.ScanStream(f, abort) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("not okay")) return } + defer r.MultipartForm.RemoveAll() result := <-response w.WriteHeader(http.StatusOK) diff --git a/clamav-http/server/v1alpha/health_handler.go b/clamav-http/server/v1alpha/health_handler.go index fb35724..b9d9955 100644 --- a/clamav-http/server/v1alpha/health_handler.go +++ b/clamav-http/server/v1alpha/health_handler.go @@ -22,8 +22,10 @@ type HealthHandler struct { func (hh *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := clamd.NewClamd(hh.Address) - response, err := c.ScanStream(strings.NewReader(eicar), make(chan bool)) + abort := make(chan bool) + defer close(abort) + response, err := c.ScanStream(strings.NewReader(eicar), abort) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) diff --git a/clamav-http/server/v1alpha/scan_handler.go b/clamav-http/server/v1alpha/scan_handler.go index 0348ec3..1c93621 100644 --- a/clamav-http/server/v1alpha/scan_handler.go +++ b/clamav-http/server/v1alpha/scan_handler.go @@ -67,12 +67,16 @@ func (sh *ScanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } c := clamd.NewClamd(sh.Address) - response, err := c.ScanStream(f, make(chan bool)) + abort := make(chan bool) + defer close(abort) + + response, err := c.ScanStream(f, abort) if err != nil { sh.Logger.Errorf("scan error %d: %s", scan_error_2, err.Error()) writeError(w, wantJSON, http.StatusInternalServerError, err.Error()) return } + defer r.MultipartForm.RemoveAll() result := <-response