Skip to content

Commit

Permalink
add FileServer and CacheControl middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed Mar 20, 2021
1 parent 23cfcdc commit 5564c20
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 0 deletions.
35 changes: 35 additions & 0 deletions cache_control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package rest

import (
"crypto/sha1" //nolint not used for cryptography
"fmt"
"net/http"
"strings"
"time"
)

// CacheControl is a middleware setting cache expiration. Using url+version for etag
func CacheControl(expiration time.Duration, version string) func(http.Handler) http.Handler {

etag := func(r *http.Request, version string) string {
s := fmt.Sprintf("%s:%s", version, r.URL.String())
return fmt.Sprintf("%x", sha1.Sum([]byte(s))) //nolint
}

return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
e := `"` + etag(r, version) + `"`
w.Header().Set("Etag", e)
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, no-cache", int(expiration.Seconds())))

if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, e) {
w.WriteHeader(http.StatusNotModified)
return
}
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
45 changes: 45 additions & 0 deletions cache_control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package rest

import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestRest_cacheControl(t *testing.T) {

tbl := []struct {
url string
version string
exp time.Duration
etag string
maxAge int
}{
{"http://example.com/foo", "v1", time.Hour, "b433be1ea19edaee9dc92ca4b895b6bdf3c058cb", 3600},
{"http://example.com/foo2", "v1", 10 * time.Hour, "6d8466aef3246c1057452561acddf7ad9d0d99e0", 36000},
{"http://example.com/foo", "v2", time.Hour, "481700c52aab0dfbca99f3ffc2a4fbb27884c114", 3600},
{"https://example.com/foo", "v2", time.Hour, "bebd4f1b87f474792c4e75e5affe31fbf67f5778", 3600},
}

for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest("GET", tt.url, nil)
w := httptest.NewRecorder()

h := CacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
h.ServeHTTP(w, req)
resp := w.Result()
assert.Equal(t, http.StatusOK, resp.StatusCode)
t.Logf("%+v", resp.Header)
assert.Equal(t, `"`+tt.etag+`"`, resp.Header.Get("Etag"))
assert.Equal(t, `max-age=`+strconv.Itoa(int(tt.exp.Seconds()))+", no-cache", resp.Header.Get("Cache-Control"))

})
}

}
49 changes: 49 additions & 0 deletions file_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package rest

import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
)

// FileServer returns http.FileServer handler to serve static files from a http.FileSystem,
// prevents directory listing.
// - public defines base path of the url, i.e. for http://example.com/static/* it should be /static
// - local for the local path to the root of the served directory
func FileServer(public, local string) (http.Handler, error) {

root, err := filepath.Abs(local)
if err != nil {
return nil, fmt.Errorf("can't get absolute path for %s: %w", local, err)
}
if _, err = os.Stat(root); os.IsNotExist(err) {
return nil, fmt.Errorf("local path %s doesn't exist: %w", root, err)
}

return http.StripPrefix(public, http.FileServer(noDirListingFS{http.Dir(root)})), nil
}

type noDirListingFS struct{ fs http.FileSystem }

// Open file on FS, for directory enforce index.html and fail on a missing index
func (fs noDirListingFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}

s, err := f.Stat()
if err != nil {
return nil, err
}

if s.IsDir() {
index := strings.TrimSuffix(name, "/") + "/index.html"
if _, err := fs.fs.Open(index); err != nil {
return nil, err
}
}
return f, nil
}
61 changes: 61 additions & 0 deletions file_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package rest

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"

"github.com/go-pkgz/rest/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFileServer(t *testing.T) {
fh, err := FileServer("/static", "./testdata/root")
require.NoError(t, err)
ts := httptest.NewServer(logger.Logger(fh))
defer ts.Close()
client := http.Client{Timeout: 599 * time.Second}

tbl := []struct {
req string
body string
status int
}{
{"/static", "testdata/index.html", 200},
{"/static/index.html", "testdata/index.html", 200},
{"/static/xyz.js", "testdata/xyz.js", 200},
{"/static/1/", "", 404},
{"/static/1/nothing", "", 404},
{"/static/1/f1.html", "testdata/1/f1.html", 200},
{"/static/2/", "testdata/2/index.html", 200},
{"/static/2", "testdata/2/index.html", 200},
{"/static/2/index.html", "testdata/2/index.html", 200},
{"/static/2/index", "", 404},
{"/static/2/f123.txt", "testdata/2/f123.txt", 200},
{"/static/1/../", "testdata/index.html", 200},
{"/static/../", "testdata/index.html", 200},
{"/static/../../", "testdata/index.html", 200},
{"/static/../../../", "testdata/index.html", 200},
}

for i, tt := range tbl {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req, err := http.NewRequest("GET", ts.URL+tt.req, nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, tt.status, resp.StatusCode)
if resp.StatusCode != http.StatusOK {
return
}
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, tt.body, string(body))

})
}
}
1 change: 1 addition & 0 deletions testdata/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
illegal!
1 change: 1 addition & 0 deletions testdata/root/1/f1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/1/f1.html
1 change: 1 addition & 0 deletions testdata/root/1/f2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/1/f2.html
1 change: 1 addition & 0 deletions testdata/root/2/f123.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/2/f123.txt
1 change: 1 addition & 0 deletions testdata/root/2/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/2/index.html
1 change: 1 addition & 0 deletions testdata/root/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/index.html
1 change: 1 addition & 0 deletions testdata/root/xyz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/xyz.js

0 comments on commit 5564c20

Please sign in to comment.