-
Notifications
You must be signed in to change notification settings - Fork 2
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
feature: add immutable option #126
base: master
Are you sure you want to change the base?
Changes from 7 commits
266aa51
0741822
dcd05c2
121192a
698b924
20bbe59
3fc3c06
548cea3
6b3ecce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,11 @@ package static | |
import ( | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
"unsafe" | ||
|
||
rrcontext "github.com/roadrunner-server/context" | ||
|
@@ -34,6 +37,147 @@ type Logger interface { | |
NamedLogger(name string) *zap.Logger | ||
} | ||
|
||
type FileServer func(ps *Plugin, next http.Handler, w http.ResponseWriter, r *http.Request, fp string) | ||
|
||
func server(s *Plugin, next http.Handler, w http.ResponseWriter, r *http.Request, fp string) { | ||
// ok, file is not in the forbidden list | ||
// Stat it and get file info | ||
f, err := s.root.Open(fp) | ||
if err != nil { | ||
// else no such file, show error in logs only in debug mode | ||
s.log.Debug("no such file or directory", zap.Error(err)) | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
// at high confidence here should not be an error | ||
// because we stat-ed the path previously and know, that that is file (not a dir), and it exists | ||
finfo, err := f.Stat() | ||
if err != nil { | ||
// else no such file, show error in logs only in debug mode | ||
s.log.Debug("no such file or directory", zap.Error(err)) | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
defer func() { | ||
err = f.Close() | ||
if err != nil { | ||
s.log.Error("file close error", zap.Error(err)) | ||
} | ||
}() | ||
|
||
// if provided path to the dir, do not serve the dir, but pass the request to the worker | ||
if finfo.IsDir() { | ||
s.log.Debug("possible path to dir provided") | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
// set etag | ||
if s.cfg.CalculateEtag { | ||
SetEtag(w, calculateEtag(s.cfg.Weak, f, finfo.Name())) | ||
} | ||
|
||
if s.cfg.Request != nil { | ||
for k, v := range s.cfg.Request { | ||
r.Header.Add(k, v) | ||
} | ||
} | ||
|
||
if s.cfg.Response != nil { | ||
for k, v := range s.cfg.Response { | ||
w.Header().Set(k, v) | ||
} | ||
} | ||
|
||
// we passed all checks - serve the file | ||
http.ServeContent(w, r, finfo.Name(), finfo.ModTime(), f) | ||
} | ||
|
||
type ScannedFile struct { | ||
file http.File | ||
name string | ||
modTime time.Time | ||
etag string | ||
} | ||
|
||
func createImmutableServer(s *Plugin) (FileServer, error) { | ||
var files map[string]ScannedFile | ||
|
||
var scanner func(path string, info os.FileInfo, err error) error | ||
scanner = func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if info.IsDir() { | ||
return filepath.Walk(info.Name(), scanner) | ||
} | ||
|
||
file, openError := s.root.Open(path) | ||
|
||
if openError != nil { | ||
return openError | ||
} | ||
|
||
var etag string | ||
|
||
if s.cfg.CalculateEtag { | ||
etag = calculateEtag(s.cfg.Weak, file, info.Name()) | ||
} | ||
|
||
files[path] = ScannedFile{ | ||
file: file, | ||
modTime: info.ModTime(), | ||
name: info.Name(), | ||
etag: etag, | ||
} | ||
|
||
return nil | ||
} | ||
|
||
err := filepath.Walk(s.cfg.Dir, scanner) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return func(s *Plugin, next http.Handler, w http.ResponseWriter, r *http.Request, fp string) { | ||
file, ok := files[fp] | ||
if ok { | ||
// else no such file, show error in logs only in debug mode | ||
s.log.Debug("no such file or directory") | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
// set etag | ||
if file.etag != "" { | ||
SetEtag(w, file.etag) | ||
} | ||
|
||
if s.cfg.Request != nil { | ||
for k, v := range s.cfg.Request { | ||
r.Header.Add(k, v) | ||
} | ||
} | ||
|
||
if s.cfg.Response != nil { | ||
for k, v := range s.cfg.Response { | ||
w.Header().Set(k, v) | ||
} | ||
} | ||
|
||
// we passed all checks - serve the file | ||
http.ServeContent(w, r, file.name, file.modTime, file.file) | ||
}, nil | ||
} | ||
|
||
// Plugin serves static files. Potentially convert into middleware? | ||
type Plugin struct { | ||
// server configuration (location, forbidden files etc) | ||
|
@@ -115,7 +259,18 @@ func (s *Plugin) Name() string { | |
} | ||
|
||
// Middleware must return true if a request/response pair is handled within the middleware. | ||
func (s *Plugin) Middleware(next http.Handler) http.Handler { //nolint:gocognit,gocyclo | ||
func (s *Plugin) Middleware(next http.Handler) http.Handler { | ||
var server FileServer = server | ||
|
||
if s.cfg.Immutable { | ||
immutableServer, err := createImmutableServer(s) | ||
if err != nil { | ||
panic(err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Panic in Go is not the same as exception in PHP, for example, and should be avoided as much as possible. All data should be initialized (since you know the list of files to serve on the Init stage) on the Init stage. Middleware, on error should delete the buggy file from the list of the served files (or try to re-fetch). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see. This way should be ok then? |
||
} | ||
|
||
server = immutableServer | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling errors without using Using if err != nil {
s.log.Error("failed to create immutable server", zap.Error(err))
return nil
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should have said that these errors can be returned from Init :P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Warxcell, you're absolutely right! Handling these errors in the Learnings added
|
||
// Define the http.HandlerFunc | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if val, ok := r.Context().Value(rrcontext.OtelTracerNameKey).(string); ok { | ||
|
@@ -170,63 +325,9 @@ func (s *Plugin) Middleware(next http.Handler) http.Handler { //nolint:gocognit, | |
|
||
// file extension allowed | ||
} | ||
|
||
// ok, file is not in the forbidden list | ||
// Stat it and get file info | ||
f, err := s.root.Open(fp) | ||
if err != nil { | ||
// else no such file, show error in logs only in debug mode | ||
s.log.Debug("no such file or directory", zap.Error(err)) | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
// at high confidence here should not be an error | ||
// because we stat-ed the path previously and know, that that is file (not a dir), and it exists | ||
finfo, err := f.Stat() | ||
if err != nil { | ||
// else no such file, show error in logs only in debug mode | ||
s.log.Debug("no such file or directory", zap.Error(err)) | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
defer func() { | ||
err = f.Close() | ||
if err != nil { | ||
s.log.Error("file close error", zap.Error(err)) | ||
} | ||
}() | ||
|
||
// if provided path to the dir, do not serve the dir, but pass the request to the worker | ||
if finfo.IsDir() { | ||
s.log.Debug("possible path to dir provided") | ||
// pass request to the worker | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
// set etag | ||
if s.cfg.CalculateEtag { | ||
SetEtag(s.cfg.Weak, f, finfo.Name(), w) | ||
} | ||
|
||
if s.cfg.Request != nil { | ||
for k, v := range s.cfg.Request { | ||
r.Header.Add(k, v) | ||
} | ||
} | ||
|
||
if s.cfg.Response != nil { | ||
for k, v := range s.cfg.Response { | ||
w.Header().Set(k, v) | ||
} | ||
} | ||
|
||
// we passed all checks - serve the file | ||
http.ServeContent(w, r, finfo.Name(), finfo.ModTime(), f) | ||
server(s, next, w, r, fp) | ||
}) | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing plugin pointer is a bad idea. Consider approach without that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like that?