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 }