clamrest.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "log"
  8. "net/http"
  9. "os"
  10. "strings"
  11. "time"
  12. "github.com/dutchcoders/go-clamd"
  13. "github.com/prometheus/client_golang/prometheus"
  14. "github.com/prometheus/client_golang/prometheus/collectors"
  15. "github.com/prometheus/client_golang/prometheus/promhttp"
  16. )
  17. var opts map[string]string
  18. func init() {
  19. log.SetOutput(ioutil.Discard)
  20. }
  21. func clamversion(w http.ResponseWriter, r *http.Request) {
  22. c := clamd.NewClamd(opts["CLAMD_PORT"])
  23. version, err := c.Version()
  24. if err != nil {
  25. errJson, eErr := json.Marshal(err)
  26. if eErr != nil {
  27. fmt.Println(eErr)
  28. return
  29. }
  30. fmt.Fprint(w, string(errJson))
  31. return
  32. }
  33. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  34. for version_string := range version {
  35. if strings.HasPrefix(version_string.Raw, "ClamAV ") {
  36. version_values := strings.Split(strings.Replace(version_string.Raw, "ClamAV ", "", 1),"/")
  37. respJson := fmt.Sprintf("{ \"Clamav\": \"%s\" }", version_values[0])
  38. if len(version_values) == 3 {
  39. respJson = fmt.Sprintf("{ \"Clamav\": \"%s\", \"Signature\": \"%s\" , \"Signature_date\": \"%s\" }", version_values[0], version_values[1], version_values[2])
  40. }
  41. fmt.Fprint(w, string(respJson))
  42. } else {
  43. w.WriteHeader(http.StatusInternalServerError)
  44. }
  45. }
  46. }
  47. func home(w http.ResponseWriter, r *http.Request) {
  48. c := clamd.NewClamd(opts["CLAMD_PORT"])
  49. response, err := c.Stats()
  50. if err != nil {
  51. errJson, eErr := json.Marshal(err)
  52. if eErr != nil {
  53. fmt.Println(eErr)
  54. return
  55. }
  56. fmt.Fprint(w, string(errJson))
  57. return
  58. }
  59. resJson, eRes := json.Marshal(response)
  60. if eRes != nil {
  61. fmt.Println(eRes)
  62. return
  63. }
  64. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  65. fmt.Fprint(w, string(resJson))
  66. }
  67. func scanPathHandler(w http.ResponseWriter, r *http.Request) {
  68. paths, ok := r.URL.Query()["path"]
  69. if !ok || len(paths[0]) < 1 {
  70. log.Println("Url Param 'path' is missing")
  71. return
  72. }
  73. path := paths[0]
  74. c := clamd.NewClamd(opts["CLAMD_PORT"])
  75. response, err := c.AllMatchScanFile(path)
  76. if err != nil {
  77. errJson, eErr := json.Marshal(err)
  78. if eErr != nil {
  79. fmt.Println(eErr)
  80. return
  81. }
  82. fmt.Fprint(w, string(errJson))
  83. return
  84. }
  85. var scanResults []*clamd.ScanResult
  86. for responseItem := range response {
  87. scanResults = append(scanResults, responseItem)
  88. }
  89. resJson, eRes := json.Marshal(scanResults)
  90. if eRes != nil {
  91. fmt.Println(eRes)
  92. return
  93. }
  94. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  95. fmt.Fprint(w, string(resJson))
  96. }
  97. //This is where the action happens.
  98. func scanHandler(w http.ResponseWriter, r *http.Request) {
  99. switch r.Method {
  100. //POST takes the uploaded file(s) and saves it to disk.
  101. case "POST":
  102. c := clamd.NewClamd(opts["CLAMD_PORT"])
  103. //get the multipart reader for the request.
  104. reader, err := r.MultipartReader()
  105. if err != nil {
  106. http.Error(w, err.Error(), http.StatusInternalServerError)
  107. return
  108. }
  109. //copy each part to destination.
  110. for {
  111. part, err := reader.NextPart()
  112. if err == io.EOF {
  113. break
  114. }
  115. //if part.FileName() is empty, skip this iteration.
  116. if part.FileName() == "" {
  117. continue
  118. }
  119. fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning: " + part.FileName() + "\n")
  120. var abort chan bool
  121. response, err := c.ScanStream(part, abort)
  122. for s := range response {
  123. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  124. respJson := fmt.Sprintf("{ \"Status\": \"%s\", \"Description\": \"%s\" }", s.Status, s.Description)
  125. switch s.Status {
  126. case clamd.RES_OK:
  127. w.WriteHeader(http.StatusOK)
  128. case clamd.RES_FOUND:
  129. w.WriteHeader(http.StatusNotAcceptable)
  130. case clamd.RES_ERROR:
  131. w.WriteHeader(http.StatusBadRequest)
  132. case clamd.RES_PARSE_ERROR:
  133. w.WriteHeader(http.StatusPreconditionFailed)
  134. default:
  135. w.WriteHeader(http.StatusNotImplemented)
  136. }
  137. fmt.Fprint(w, respJson)
  138. fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for: %v, %v\n", part.FileName(), s)
  139. }
  140. fmt.Printf(time.Now().Format(time.RFC3339) + " Finished scanning: " + part.FileName() + "\n")
  141. }
  142. default:
  143. w.WriteHeader(http.StatusMethodNotAllowed)
  144. }
  145. }
  146. func scanHandlerBody(w http.ResponseWriter, r *http.Request) {
  147. if r.Method != "POST" {
  148. w.WriteHeader(http.StatusMethodNotAllowed)
  149. return
  150. }
  151. c := clamd.NewClamd(opts["CLAMD_PORT"])
  152. fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning plain body\n")
  153. var abort chan bool
  154. defer r.Body.Close()
  155. response, _ := c.ScanStream(r.Body, abort)
  156. for s := range response {
  157. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  158. respJson := fmt.Sprintf("{ Status: %q, Description: %q }", s.Status, s.Description)
  159. switch s.Status {
  160. case clamd.RES_OK:
  161. w.WriteHeader(http.StatusOK)
  162. case clamd.RES_FOUND:
  163. w.WriteHeader(http.StatusNotAcceptable)
  164. case clamd.RES_ERROR:
  165. w.WriteHeader(http.StatusBadRequest)
  166. case clamd.RES_PARSE_ERROR:
  167. w.WriteHeader(http.StatusPreconditionFailed)
  168. default:
  169. w.WriteHeader(http.StatusNotImplemented)
  170. }
  171. fmt.Fprint(w, respJson)
  172. fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for plain body: %v\n", s)
  173. }
  174. }
  175. func waitForClamD(port string, times int) {
  176. clamdTest := clamd.NewClamd(port)
  177. clamdTest.Ping()
  178. version, err := clamdTest.Version()
  179. if err != nil {
  180. if times < 30 {
  181. fmt.Printf("clamD not running, waiting times [%v]\n", times)
  182. time.Sleep(time.Second * 4)
  183. waitForClamD(port, times+1)
  184. } else {
  185. fmt.Printf("Error getting clamd version: %v\n", err)
  186. os.Exit(1)
  187. }
  188. } else {
  189. for version_string := range version {
  190. fmt.Printf("Clamd version: %#v\n", version_string.Raw)
  191. }
  192. }
  193. }
  194. func main() {
  195. opts = make(map[string]string)
  196. // https://github.com/prometheus/client_golang/blob/main/examples/gocollector/main.go
  197. reg := prometheus.NewRegistry()
  198. reg.MustRegister(collectors.NewBuildInfoCollector())
  199. reg.MustRegister(collectors.NewGoCollector(
  200. collectors.WithGoCollections(collectors.GoRuntimeMemStatsCollection | collectors.GoRuntimeMetricsCollection),
  201. ))
  202. for _, e := range os.Environ() {
  203. pair := strings.Split(e, "=")
  204. opts[pair[0]] = pair[1]
  205. }
  206. if opts["CLAMD_PORT"] == "" {
  207. opts["CLAMD_PORT"] = "tcp://localhost:3310"
  208. }
  209. fmt.Printf("Starting clamav rest bridge\n")
  210. fmt.Printf("Connecting to clamd on %v\n", opts["CLAMD_PORT"])
  211. waitForClamD(opts["CLAMD_PORT"], 1)
  212. fmt.Printf("Connected to clamd on %v\n", opts["CLAMD_PORT"])
  213. http.HandleFunc("/scan", scanHandler)
  214. http.HandleFunc("/scanPath", scanPathHandler)
  215. http.HandleFunc("/scanHandlerBody", scanHandlerBody)
  216. http.HandleFunc("/version", clamversion)
  217. http.HandleFunc("/", home)
  218. // Prometheus metrics
  219. http.Handle("/metrics", promhttp.HandlerFor(
  220. reg,
  221. promhttp.HandlerOpts{
  222. // Opt into OpenMetrics to support exemplars.
  223. EnableOpenMetrics: true,
  224. },
  225. ))
  226. // Start the HTTPS server in a goroutine
  227. go http.ListenAndServeTLS(fmt.Sprintf(":%s", opts["SSL_PORT"]), "/etc/ssl/clamav-rest/server.crt", "/etc/ssl/clamav-rest/server.key", nil)
  228. // Start the HTTP server
  229. http.ListenAndServe(fmt.Sprintf(":%s", opts["PORT"]), nil)
  230. }