net/http: add ServeFileFS, FileServerFS, NewFileTransportFS

These new apis are analogous to ServeFile, FileServer and NewFileTransport respectively. The main difference is that these functions operate on an fs.FS.

Fixes #51971

Change-Id: Ie56b245b795eeb7edf613657578592306945469b
GitHub-Last-Rev: 26e75c0368
GitHub-Pull-Request: golang/go#61641
Reviewed-on: https://go-review.googlesource.com/c/go/+/513956
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: David Chase <drchase@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Mauri de Souza Meneguzzo 2023-07-31 20:58:45 +00:00 committed by Damien Neil
parent 4e728e5121
commit 65d4723b49
5 changed files with 160 additions and 3 deletions

3
api/next/51971.txt Normal file
View file

@ -0,0 +1,3 @@
pkg net/http, func ServeFileFS(ResponseWriter, *Request, fs.FS, string) #51971
pkg net/http, func FileServerFS(fs.FS) Handler #51971
pkg net/http, func NewFileTransportFS(fs.FS) RoundTripper #51971

View file

@ -7,6 +7,7 @@ package http
import (
"fmt"
"io"
"io/fs"
)
// fileTransport implements RoundTripper for the 'file' protocol.
@ -31,6 +32,24 @@ func NewFileTransport(fs FileSystem) RoundTripper {
return fileTransport{fileHandler{fs}}
}
// NewFileTransportFS returns a new RoundTripper, serving the provided
// file system fsys. The returned RoundTripper ignores the URL host in its
// incoming requests, as well as most other properties of the
// request.
//
// The typical use case for NewFileTransportFS is to register the "file"
// protocol with a Transport, as in:
//
// fsys := os.DirFS("/")
// t := &http.Transport{}
// t.RegisterProtocol("file", http.NewFileTransportFS(fsys))
// c := &http.Client{Transport: t}
// res, err := c.Get("file:///etc/passwd")
// ...
func NewFileTransportFS(fsys fs.FS) RoundTripper {
return NewFileTransport(FS(fsys))
}
func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) {
// We start ServeHTTP in a goroutine, which may take a long
// time if the file is large. The newPopulateResponseWriter

View file

@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"testing"
"testing/fstest"
)
func checker(t *testing.T) func(string, error) {
@ -62,3 +63,44 @@ func TestFileTransport(t *testing.T) {
}
res.Body.Close()
}
func TestFileTransportFS(t *testing.T) {
check := checker(t)
fsys := fstest.MapFS{
"index.html": {Data: []byte("index.html says hello")},
}
tr := &Transport{}
tr.RegisterProtocol("file", NewFileTransportFS(fsys))
c := &Client{Transport: tr}
for fname, mfile := range fsys {
urlstr := "file:///" + fname
res, err := c.Get(urlstr)
check("Get "+urlstr, err)
if res.StatusCode != 200 {
t.Errorf("for %s, StatusCode = %d, want 200", urlstr, res.StatusCode)
}
if res.ContentLength != -1 {
t.Errorf("for %s, ContentLength = %d, want -1", urlstr, res.ContentLength)
}
if res.Body == nil {
t.Fatalf("for %s, nil Body", urlstr)
}
slurp, err := io.ReadAll(res.Body)
res.Body.Close()
check("ReadAll "+urlstr, err)
if string(slurp) != string(mfile.Data) {
t.Errorf("for %s, got content %q, want %q", urlstr, string(slurp), "Bar")
}
}
const badURL = "file://../no-exist.txt"
res, err := c.Get(badURL)
check("Get "+badURL, err)
if res.StatusCode != 404 {
t.Errorf("for %s, StatusCode = %d, want 404", badURL, res.StatusCode)
}
res.Body.Close()
}

View file

@ -741,6 +741,40 @@ func ServeFile(w ResponseWriter, r *Request, name string) {
serveFile(w, r, Dir(dir), file, false)
}
// ServeFileFS replies to the request with the contents
// of the named file or directory from the file system fsys.
//
// If the provided file or directory name is a relative path, it is
// interpreted relative to the current directory and may ascend to
// parent directories. If the provided name is constructed from user
// input, it should be sanitized before calling ServeFile.
//
// As a precaution, ServeFile will reject requests where r.URL.Path
// contains a ".." path element; this protects against callers who
// might unsafely use filepath.Join on r.URL.Path without sanitizing
// it and then use that filepath.Join result as the name argument.
//
// As another special case, ServeFile redirects any request where r.URL.Path
// ends in "/index.html" to the same path, without the final
// "index.html". To avoid such redirects either modify the path or
// use ServeContent.
//
// Outside of those two special cases, ServeFile does not use
// r.URL.Path for selecting the file or directory to serve; only the
// file or directory provided in the name argument is used.
func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) {
if containsDotDot(r.URL.Path) {
// Too many programs use r.URL.Path to construct the argument to
// serveFile. Reject the request under the assumption that happened
// here and ".." may not be wanted.
// Note that name might not contain "..", for example if code (still
// incorrectly) used filepath.Join(myDir, r.URL.Path).
Error(w, "invalid URL path", StatusBadRequest)
return
}
serveFile(w, r, FS(fsys), name, false)
}
func containsDotDot(v string) bool {
if !strings.Contains(v, "..") {
return false
@ -850,13 +884,23 @@ func FS(fsys fs.FS) FileSystem {
//
// http.Handle("/", http.FileServer(http.Dir("/tmp")))
//
// To use an fs.FS implementation, use http.FS to convert it:
//
// http.Handle("/", http.FileServer(http.FS(fsys)))
// To use an fs.FS implementation, use http.FileServerFS instead.
func FileServer(root FileSystem) Handler {
return &fileHandler{root}
}
// FileServerFS returns a handler that serves HTTP requests
// with the contents of the file system fsys.
//
// As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final
// "index.html".
//
// http.Handle("/", http.FileServerFS(fsys))
func FileServerFS(root fs.FS) Handler {
return FileServer(FS(root))
}
func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
upath := r.URL.Path
if !strings.HasPrefix(upath, "/") {

View file

@ -26,6 +26,7 @@ import (
"runtime"
"strings"
"testing"
"testing/fstest"
"time"
)
@ -1559,3 +1560,51 @@ func testFileServerMethods(t *testing.T, mode testMode) {
}
}
}
func TestFileServerFS(t *testing.T) {
filename := "index.html"
contents := []byte("index.html says hello")
fsys := fstest.MapFS{
filename: {Data: contents},
}
ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
defer ts.Close()
res, err := ts.Client().Get(ts.URL + "/" + filename)
if err != nil {
t.Fatal(err)
}
b, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal("reading Body:", err)
}
if s := string(b); s != string(contents) {
t.Errorf("for path %q got %q, want %q", filename, s, contents)
}
res.Body.Close()
}
func TestServeFileFS(t *testing.T) {
filename := "index.html"
contents := []byte("index.html says hello")
fsys := fstest.MapFS{
filename: {Data: contents},
}
ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
ServeFileFS(w, r, fsys, filename)
})).ts
defer ts.Close()
res, err := ts.Client().Get(ts.URL + "/" + filename)
if err != nil {
t.Fatal(err)
}
b, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal("reading Body:", err)
}
if s := string(b); s != string(contents) {
t.Errorf("for path %q got %q, want %q", filename, s, contents)
}
res.Body.Close()
}