github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/fileserver.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package safehttp
    16  
    17  import (
    18  	"errors"
    19  	"net/http"
    20  )
    21  
    22  // FileServer returns a handler that serves HTTP requests with the contents of
    23  // the file system rooted at root.
    24  func FileServer(root string) Handler {
    25  	fileServer := http.FileServer(http.Dir(root))
    26  
    27  	return HandlerFunc(func(rw ResponseWriter, req *IncomingRequest) Result {
    28  		fsrw := &fileServerResponseWriter{flight: rw.(*flight), header: http.Header{}}
    29  		fileServer.ServeHTTP(fsrw, req.req)
    30  		return fsrw.result
    31  	})
    32  }
    33  
    34  type fileServerResponseWriter struct {
    35  	flight *flight
    36  	result Result
    37  
    38  	// We don't allow direct access to the flight's underlying http.Header. We
    39  	// just copy over the contents on a call to WriteHeader, with the exception
    40  	// of the Content-Type header.
    41  	header http.Header
    42  
    43  	// Once WriteHeader is called, any subsequent calls to it are no-ops.
    44  	committed bool
    45  
    46  	// If the first call to WriteHeader is not a 200 OK, we call
    47  	// flight.WriteError with a 404 StatusCode and make further calls to Write
    48  	// no-ops in order to not leak information about the filesystem.
    49  	errored bool
    50  }
    51  
    52  func (fsrw *fileServerResponseWriter) Header() http.Header {
    53  	return fsrw.header
    54  }
    55  
    56  func (fsrw *fileServerResponseWriter) Write(b []byte) (int, error) {
    57  	if !fsrw.committed {
    58  		fsrw.WriteHeader(int(StatusOK))
    59  	}
    60  
    61  	if fsrw.errored {
    62  		// Let the framework handle the error.
    63  		return 0, errors.New("discarded")
    64  	}
    65  	return fsrw.flight.rw.Write(b)
    66  }
    67  
    68  func (fsrw *fileServerResponseWriter) WriteHeader(statusCode int) {
    69  	if fsrw.committed {
    70  		// We've already committed to a response. The headers and status code
    71  		// were written. Ignore this call.
    72  		return
    73  	}
    74  	fsrw.committed = true
    75  
    76  	headers := fsrw.flight.Header()
    77  	ct := "application/octet-stream; charset=utf-8"
    78  	if len(fsrw.header["Content-Type"]) > 0 {
    79  		ct = fsrw.header["Content-Type"][0]
    80  	}
    81  	// Content-Type should have been set by the http.FileServer.
    82  	// Note: Add or Set might panic if a header has been already claimed. This
    83  	// is intended behavior.
    84  	for k, v := range fsrw.header {
    85  		if len(v) == 0 {
    86  			continue
    87  		}
    88  		if k == "Content-Type" {
    89  			// Skip setting the Content-Type. The Dispatcher handles it.
    90  			continue
    91  		}
    92  		if headers.IsClaimed(k) {
    93  			continue
    94  		}
    95  		headers.Del(k)
    96  		for _, vv := range v {
    97  			headers.Add(k, vv)
    98  		}
    99  	}
   100  
   101  	if statusCode != int(StatusOK) {
   102  		fsrw.errored = true
   103  		// We are writing 404 for every error to avoid leaking information about
   104  		// the filesystem.
   105  		fsrw.result = fsrw.flight.WriteError(StatusNotFound)
   106  		return
   107  	}
   108  
   109  	fsrw.result = fsrw.flight.Write(FileServerResponse{
   110  		Path:        fsrw.flight.req.URL().Path(),
   111  		contentType: ct,
   112  	})
   113  }
   114  
   115  // FileServerResponse represents a FileServer response.
   116  type FileServerResponse struct {
   117  	// The URL path.
   118  	Path string
   119  
   120  	// private, to not allow modifications
   121  	contentType string
   122  }
   123  
   124  // ContentType is the Content-Type of the response.
   125  func (resp FileServerResponse) ContentType() string {
   126  	return resp.contentType
   127  }