Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Single Page Application example in README.md file #678

Merged
merged 10 commits into from
Aug 16, 2023
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,18 +251,16 @@ type spaHandler struct {
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path, err := filepath.Abs(r.URL.Path)
var err error
amustaque97 marked this conversation as resolved.
Show resolved Hide resolved
// Join internally call path.Clean to prevent directory traversal
path = filepath.Join(h.staticPath, path)
amustaque97 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// if we failed to get the absolute path respond with a 400 bad request
// and stop
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// prepend the path with the path to the static directory
path = filepath.Join(h.staticPath, path)

// check whether a file exists at the given path
_, err = os.Stat(path)
if os.IsNotExist(err) {
Expand Down
161 changes: 161 additions & 0 deletions mux_httpserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,95 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)

// spaHandler implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type spaHandler struct {
staticPath string
indexPath string
}

// FilepathAbsServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
// This is a negative test case where `filepath.Abs` will return path value like `D:\`
// if our route is `/`. As per docs: Abs returns an absolute representation of path.
// If the path is not absolute it will be joined with the current working directory to turn
// it into an absolute path. The absolute path name for a given file is not guaranteed to
// be unique. Abs calls Clean on the result.
func (h spaHandler) FilepathAbsServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path, err := filepath.Abs(r.URL.Path)
if err != nil {
// if we failed to get the absolute path respond with a 400 bad request
// and stop
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// prepend the path with the path to the static directory
path = filepath.Join(h.staticPath, path)

// check whether a file exists at the given path
_, err = os.Stat(path)

if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

// ServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
// internally calls path.Clean path to prevent directory traversal
path := filepath.Join(h.staticPath, r.URL.Path)
if err != nil {
// if we failed to get the absolute path respond with a 400 bad request
// and stop
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// check whether a file exists at the given path
_, err = os.Stat(path)

if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

func TestSchemeMatchers(t *testing.T) {
router := NewRouter()
router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -48,3 +134,78 @@ func TestSchemeMatchers(t *testing.T) {
assertResponseBody(t, s, "hello https world")
})
}

func TestServeHttpFilepathAbs(t *testing.T) {
// create a diretory name `build`
os.Mkdir("build", 0700)

// create a file `index.html` and write below content
htmlContent := []byte(`<html><head><title>hello</title></head><body>world</body></html>`)
err := ioutil.WriteFile("./build/index.html", htmlContent, 0700)
if err != nil {
t.Fatal(err)
}

// make new request
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}

// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
rr := httptest.NewRecorder()
spa := spaHandler{staticPath: "./build", indexPath: "index.html"}
spa.FilepathAbsServeHTTP(rr, req)

status := rr.Code
if runtime.GOOS != "windows" && status != http.StatusOK {
amustaque97 marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
} else if runtime.GOOS == "windows" && rr.Code != http.StatusInternalServerError {
t.Errorf("handler returned wrong status code in case of windows: got %v want %v",
status, http.StatusOK)
}

// Check the response body is what we expect.
if runtime.GOOS != "windows" && rr.Body.String() != string(htmlContent) {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), string(htmlContent))
} else if runtime.GOOS == "windows" && !strings.Contains(rr.Body.String(), "syntax is incorrect.") {
t.Errorf("handler returned unexpected body in case of windows: got %v want %v",
rr.Body.String(), string(htmlContent))
}
}

func TestServeHttpFilepathJoin(t *testing.T) {
// create a diretory name `build`
os.Mkdir("build", 0700)

// create a file `index.html` and write below content
htmlContent := []byte(`<html><head><title>hello</title></head><body>world</body></html>`)
err := ioutil.WriteFile("./build/index.html", htmlContent, 0700)
if err != nil {
t.Fatal(err)
}

// make new request
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}

// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
rr := httptest.NewRecorder()
spa := spaHandler{staticPath: "./build", indexPath: "index.html"}
spa.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}

// Check the response body is what we expect.
if rr.Body.String() != string(htmlContent) {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), string(htmlContent))
}
}