-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add FileServer and CacheControl middleware
- Loading branch information
Showing
11 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
|
||
}) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
|
||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
illegal! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/1/f1.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/1/f2.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/2/f123.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/2/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
testdata/xyz.js |