/* The xSendFile middleware transparently sends static files in HTTP responses via the X-Sendfile mechanism. All that is needed in the Rails code is the 'send_file' method. */ package sendfile import ( "fmt" "io" "net/http" "regexp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/mask" "gitlab.com/gitlab-org/gitlab/workhorse/internal/headers" "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper" "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail" "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/nginx" ) var ( sendFileRequests = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "gitlab_workhorse_sendfile_requests", Help: "How many X-Sendfile requests have been processed by gitlab-workhorse, partitioned by sendfile type.", }, []string{"type"}, ) sendFileBytes = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "gitlab_workhorse_sendfile_bytes", Help: "How many X-Sendfile bytes have been sent by gitlab-workhorse, partitioned by sendfile type.", }, []string{"type"}, ) artifactsSendFile = regexp.MustCompile("builds/[0-9]+/artifacts") ) type sendFileResponseWriter struct { rw http.ResponseWriter status int hijacked bool req *http.Request } func SendFile(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { s := &sendFileResponseWriter{ rw: rw, req: req, } // Advertise to upstream (Rails) that we support X-Sendfile req.Header.Set(headers.XSendFileTypeHeader, headers.XSendFileHeader) defer s.flush() h.ServeHTTP(s, req) }) } func (s *sendFileResponseWriter) Header() http.Header { return s.rw.Header() } func (s *sendFileResponseWriter) Write(data []byte) (int, error) { if s.status == 0 { s.WriteHeader(http.StatusOK) } if s.hijacked { return len(data), nil } return s.rw.Write(data) } func (s *sendFileResponseWriter) WriteHeader(status int) { if s.status != 0 { return } s.status = status if s.status != http.StatusOK { s.rw.WriteHeader(s.status) return } file := s.Header().Get(headers.XSendFileHeader) if file != "" && !s.hijacked { // Mark this connection as hijacked s.hijacked = true // Serve the file nginx.DisableResponseBuffering(s.rw) sendFileFromDisk(s.rw, s.req, file) return } s.rw.WriteHeader(s.status) } func sendFileFromDisk(w http.ResponseWriter, r *http.Request, file string) { log.WithContextFields(r.Context(), log.Fields{ "file": file, "method": r.Method, "uri": mask.URL(r.RequestURI), }).Print("Send file") contentTypeHeaderPresent := false if headers.IsDetectContentTypeHeaderPresent(w) { // Removing the GitlabWorkhorseDetectContentTypeHeader header to // avoid handling the response by the senddata handler w.Header().Del(headers.GitlabWorkhorseDetectContentTypeHeader) contentTypeHeaderPresent = true } content, fi, err := helper.OpenFile(file) if err != nil { http.NotFound(w, r) return } defer content.Close() countSendFileMetrics(fi.Size(), r) if contentTypeHeaderPresent { data, err := io.ReadAll(io.LimitReader(content, headers.MaxDetectSize)) if err != nil { fail.Request(w, r, fmt.Errorf("content type detection: %v", err)) return } content.Seek(0, io.SeekStart) contentType, contentDisposition := headers.SafeContentHeaders(data, w.Header().Get(headers.ContentDispositionHeader)) w.Header().Set(headers.ContentTypeHeader, contentType) w.Header().Set(headers.ContentDispositionHeader, contentDisposition) } http.ServeContent(w, r, "", fi.ModTime(), content) } func countSendFileMetrics(size int64, r *http.Request) { var requestType string switch { case artifactsSendFile.MatchString(r.RequestURI): requestType = "artifacts" default: requestType = "other" } sendFileRequests.WithLabelValues(requestType).Inc() sendFileBytes.WithLabelValues(requestType).Add(float64(size)) } func (s *sendFileResponseWriter) flush() { s.WriteHeader(http.StatusOK) }