diff options
-rw-r--r-- | cmd/rr-php/cmd/serve.go | 10 | ||||
-rw-r--r-- | http/data.go | 67 | ||||
-rw-r--r-- | http/request.go | 137 | ||||
-rw-r--r-- | http/response.go | 42 | ||||
-rw-r--r-- | http/server.go | 82 | ||||
-rw-r--r-- | http/static.go | 70 | ||||
-rw-r--r-- | http/uploads.go | 162 | ||||
-rw-r--r-- | payload.go | 2 | ||||
-rw-r--r-- | psr7/files.go | 58 | ||||
-rw-r--r-- | psr7/post.go | 45 | ||||
-rw-r--r-- | psr7/request.go | 127 | ||||
-rw-r--r-- | psr7/response.go | 34 | ||||
-rw-r--r-- | psr7/server.go | 129 |
13 files changed, 567 insertions, 398 deletions
diff --git a/cmd/rr-php/cmd/serve.go b/cmd/rr-php/cmd/serve.go index 69b53e28..24ec4043 100644 --- a/cmd/rr-php/cmd/serve.go +++ b/cmd/rr-php/cmd/serve.go @@ -20,8 +20,9 @@ import ( "os/exec" "time" "github.com/sirupsen/logrus" - rrhttp "github.com/spiral/roadrunner/psr7" + rrttp "github.com/spiral/roadrunner/http" "net/http" + "os" ) func init() { @@ -61,7 +62,7 @@ func serveHandler(cmd *cobra.Command, args []string) { // Addr: ":8080", // Handler: rrhttp.NewServer( // rrhttp.Config{ - // ServeStatic: true, + // serveStatic: true, // Root: "/Users/wolfy-j/Projects/phpapp/webroot", // }, // rr, @@ -73,10 +74,11 @@ func serveHandler(cmd *cobra.Command, args []string) { //http2.ConfigureServer(&srv, nil) //srv.ListenAndServeTLS("localhost.cert", "localhost.key") - http.ListenAndServe(":8080", rrhttp.NewServer( - rrhttp.Config{ + http.ListenAndServe(":8080", rrttp.NewServer( + rrttp.Config{ ServeStatic: true, Root: "/Users/wolfy-j/Projects/phpapp/webroot", + UploadsDir: os.TempDir(), }, rr, )) diff --git a/http/data.go b/http/data.go new file mode 100644 index 00000000..865e4760 --- /dev/null +++ b/http/data.go @@ -0,0 +1,67 @@ +package http + +import ( + "strings" + "net/http" +) + +const maxLevel = 127 + +type dataTree map[string]interface{} + +// parsePost parses incoming request body into data tree. +func parsePost(r *http.Request) (dataTree, error) { + data := make(dataTree) + + for k, v := range r.PostForm { + data.push(k, v) + } + + for k, v := range r.MultipartForm.Value { + data.push(k, v) + } + + return data, nil +} + +func (d dataTree) push(k string, v []string) { + if len(v) == 0 { + // skip empty values + return + } + + indexes := make([]string, 0) + for _, index := range strings.Split(k, "[") { + indexes = append(indexes, strings.Trim(index, "]")) + } + + if len(indexes) <= maxLevel { + d.mount(indexes, v) + } +} + +// mount mounts data tree recursively. +func (d dataTree) mount(i []string, v []string) { + if len(v) == 0 || v[0] == "" { + return + } + + if len(i) == 1 { + // single value context + d[i[0]] = v[0] + return + } + + if len(i) == 2 && i[1] == "" { + // non associated array of elements + d[i[0]] = v + return + } + + if p, ok := d[i[0]]; ok { + p.(dataTree).mount(i[1:], v) + } + + d[i[0]] = make(dataTree) + d[i[0]].(dataTree).mount(i[1:], v) +} diff --git a/http/request.go b/http/request.go new file mode 100644 index 00000000..572d7d6a --- /dev/null +++ b/http/request.go @@ -0,0 +1,137 @@ +package http + +import ( + "net/http" + "encoding/json" + "github.com/spiral/roadrunner" + "strings" + "io/ioutil" + "fmt" +) + +const ( + defaultMaxMemory = 32 << 20 // 32 MB +) + +// Request maps net/http requests to PSR7 compatible structure and managed state of temporary uploaded files. +type Request struct { + // Protocol includes HTTP protocol version. + Protocol string `json:"protocol"` + + // Method contains name of HTTP method used for the request. + Method string `json:"method"` + + // Uri contains full request Uri with scheme and query. + Uri string `json:"uri"` + + // Headers contains list of request headers. + Headers http.Header `json:"headers"` + + // Cookies contains list of request cookies. + Cookies map[string]string `json:"cookies"` + + // RawQuery contains non parsed query string (to be parsed on php end). + RawQuery string `json:"rawQuery"` + + // Parsed indicates that request body has been parsed on RR end. + Parsed bool `json:"parsed"` + + // Uploads contains list of uploaded files, their names, sized and associations with temporary files. + Uploads *Uploads `json:"uploads"` + + // request body can be parsedData or []byte + body interface{} +} + +// NewRequest creates new PSR7 compatible request using net/http request. +func NewRequest(r *http.Request) (req *Request, err error) { + req = &Request{ + Protocol: r.Proto, + Method: r.Method, + Uri: uri(r), + Headers: r.Header, + Cookies: make(map[string]string), + RawQuery: r.URL.RawQuery, + } + + for _, c := range r.Cookies() { + req.Cookies[c.Name] = c.Value + } + + if !req.parsable() { + req.body, err = ioutil.ReadAll(r.Body) + return req, err + } + + if err = r.ParseMultipartForm(defaultMaxMemory); err != nil { + return nil, err + } + + if req.body, err = parsePost(r); err != nil { + return nil, err + } + + if req.Uploads, err = parseUploads(r); err != nil { + return nil, err + } + + req.Parsed = true + return req, nil +} + +// OpenUploads moves all uploaded files to temporary directory so it can be given to php later. +func (r *Request) OpenUploads(tmpDir string) error { + if r.Uploads == nil { + return nil + } + + return r.Uploads.OpenUploads(tmpDir) +} + +// Close clears all temp file uploads +func (r *Request) Close() { + if r.Uploads == nil { + return + } + + r.Uploads.Clear() +} + +// Payload request marshaled RoadRunner payload based on PSR7 data. Default encode method is JSON. Make sure to open +// files prior to calling this method. +func (r *Request) Payload() (p *roadrunner.Payload, err error) { + p = &roadrunner.Payload{} + + if p.Context, err = json.Marshal(r); err != nil { + return nil, err + } + + if r.Parsed { + if p.Body, err = json.Marshal(r.body); err != nil { + return nil, err + } + } else if r.body != nil { + p.Body = r.body.([]byte) + } + + return p, nil +} + +// parsable returns true if request payload can be parsed (POST dataTree, file tree). +func (r *Request) parsable() bool { + if r.Method != "POST" && r.Method != "PUT" && r.Method != "PATCH" { + return false + } + + ct := r.Headers.Get("content-type") + return strings.Contains(ct, "multipart/form-data") || ct == "application/x-www-form-urlencoded" +} + +// uri fetches full uri from request in a form of string (including https scheme if TLS connection is enabled). +func uri(r *http.Request) string { + if r.TLS != nil { + return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) + } + + return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) +} diff --git a/http/response.go b/http/response.go new file mode 100644 index 00000000..d35669c4 --- /dev/null +++ b/http/response.go @@ -0,0 +1,42 @@ +package http + +import ( + "net/http" + "encoding/json" + "github.com/spiral/roadrunner" +) + +// Response handles PSR7 response logic. +type Response struct { + // Status contains response status. + Status int `json:"status"` + + // Headers contains list of response headers. + Headers map[string][]string `json:"headers"` + + // associated body payload. + body []byte +} + +// NewResponse creates new response based on given roadrunner payload. +func NewResponse(p *roadrunner.Payload) (*Response, error) { + r := &Response{body: p.Body} + if err := json.Unmarshal(p.Context, r); err != nil { + return nil, err + } + + return r, nil +} + +// Write writes response headers, status and body into ResponseWriter. +func (r *Response) Write(w http.ResponseWriter) { + for k, v := range r.Headers { + for _, h := range v { + w.Header().Add(k, h) + + } + } + + w.WriteHeader(r.Status) + w.Write(r.body) +} diff --git a/http/server.go b/http/server.go new file mode 100644 index 00000000..363dca2d --- /dev/null +++ b/http/server.go @@ -0,0 +1,82 @@ +package http + +import ( + "github.com/spiral/roadrunner" + "net/http" +) + +// Configures RoadRunner HTTP server. +type Config struct { + // serve enables static file serving from desired root directory. + ServeStatic bool + + // Root directory, required when serve set to true. + Root string + + // UploadsDir contains name of temporary directory to store uploaded files passed to underlying PHP process. + UploadsDir string +} + +// Server serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, +// parsed files and query, payload will include parsed form dataTree (if any). +type Server struct { + cfg Config + static *staticServer + rr *roadrunner.Server +} + +// NewServer returns new instance of HTTP PSR7 server. +func NewServer(cfg Config, server *roadrunner.Server) *Server { + h := &Server{cfg: cfg, rr: server} + + if cfg.ServeStatic { + h.static = &staticServer{root: http.Dir(h.cfg.Root)} + } + + return h +} + +// ServeHTTP serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled. +func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () { + if srv.cfg.ServeStatic && srv.static.serve(w, r) { + return + } + + req, err := NewRequest(r) + if err != nil { + srv.sendError(w, r, err) + return + } + + if err = req.OpenUploads(srv.cfg.UploadsDir); err != nil { + srv.sendError(w, r, err) + return + } + defer req.Close() + + p, err := req.Payload() + if err != nil { + srv.sendError(w, r, err) + return + } + + rsp, err := srv.rr.Exec(p) + if err != nil { + srv.sendError(w, r, err) + return + } + + resp, err := NewResponse(rsp) + if err != nil { + srv.sendError(w, r, err) + return + } + + resp.Write(w) +} + +// sendError sends error +func (srv *Server) sendError(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(500) + w.Write([]byte(err.Error())) +} diff --git a/http/static.go b/http/static.go new file mode 100644 index 00000000..bfcb87c2 --- /dev/null +++ b/http/static.go @@ -0,0 +1,70 @@ +package http + +import ( + "net/http" + "strings" + "path" + "github.com/sirupsen/logrus" + "os" + "path/filepath" +) + +var ( + forbiddenFiles = []string{".php", ".htaccess"} +) + +// staticServer serves static files +type staticServer struct { + root http.Dir +} + +// serve attempts to serve static file and returns true in case of success, will return false in case if file not +// found, not allowed or on read error. +func (svr *staticServer) serve(w http.ResponseWriter, r *http.Request) bool { + fpath := r.URL.Path + if !strings.HasPrefix(fpath, "/") { + fpath = "/" + fpath + } + fpath = path.Clean(fpath) + + if svr.forbidden(fpath) { + logrus.Warningf("attempt to access forbidden file %s", fpath) // todo: better logs + return false + } + + f, err := svr.root.Open(fpath) + if err != nil { + if !os.IsNotExist(err) { + logrus.Error(err) //todo: rr or access error + } + + return false + } + defer f.Close() + + d, err := f.Stat() + if err != nil { + logrus.Error(err) //todo: rr or access error + return false + } + + if d.IsDir() { + // do not serve directories + return false + } + + http.ServeContent(w, r, d.Name(), d.ModTime(), f) + return true +} + +// forbidden returns true if file has forbidden extension. +func (svr *staticServer) forbidden(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + for _, exl := range forbiddenFiles { + if ext == exl { + return true + } + } + + return false +} diff --git a/http/uploads.go b/http/uploads.go new file mode 100644 index 00000000..1b851e6e --- /dev/null +++ b/http/uploads.go @@ -0,0 +1,162 @@ +package http + +import ( + "mime/multipart" + "encoding/json" + "log" + "strings" + "net/http" + "io/ioutil" + "io" + "sync" +) + +// FileUpload represents singular file wrapUpload. +type FileUpload struct { + // Name contains filename specified by the client. + Name string `json:"name"` + + // MimeType contains mime-type provided by the client. + MimeType string `json:"mimetype"` + + // Size of the uploaded file. + Size int64 `json:"size"` + + // Error indicates file upload error (if any). See http://php.net/manual/en/features.file-upload.errors.php + Error int + + // TempFilename points to temporary file location. + TempFilename string `json:"tempFilename"` + + // associated file header + header *multipart.FileHeader +} + +func (f *FileUpload) Open(tmpDir string) error { + file, err := f.header.Open() + if err != nil { + return err + } + + defer file.Close() + + tmp, err := ioutil.TempFile(tmpDir, "upload") + if err != nil { + return err + } + + f.TempFilename = tmp.Name() + defer tmp.Close() + + f.Size, err = io.Copy(tmp, file) + return err +} + +func wrapUpload(f *multipart.FileHeader) *FileUpload { + log.Print(f.Header) + return &FileUpload{ + Name: f.Filename, + MimeType: f.Header.Get("Content-Type"), + header: f, + } +} + +type fileTree map[string]interface{} + +func (d fileTree) push(k string, v []*FileUpload) { + if len(v) == 0 { + // skip empty values + return + } + + indexes := make([]string, 0) + for _, index := range strings.Split(k, "[") { + indexes = append(indexes, strings.Trim(index, "]")) + } + + if len(indexes) <= maxLevel { + d.mount(indexes, v) + } +} + +// mount mounts data tree recursively. +func (d fileTree) mount(i []string, v []*FileUpload) { + if len(v) == 0 { + return + } + + if len(i) == 1 { + // single value context + d[i[0]] = v[0] + return + } + + if len(i) == 2 && i[1] == "" { + // non associated array of elements + d[i[0]] = v + return + } + + if p, ok := d[i[0]]; ok { + p.(fileTree).mount(i[1:], v) + } + + d[i[0]] = make(fileTree) + d[i[0]].(fileTree).mount(i[1:], v) +} + +// tree manages uploaded files tree and temporary files. +type Uploads struct { + // pre processed data tree for Uploads. + tree fileTree + + // flat list of all file Uploads. + list []*FileUpload +} + +// MarshalJSON marshal tree tree into JSON. +func (u *Uploads) MarshalJSON() ([]byte, error) { + return json.Marshal(u.tree) +} + +// OpenUploads moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors +// will be handled individually. @todo: do we need it? +func (u *Uploads) OpenUploads(tmpDir string) error { + var wg sync.WaitGroup + for _, f := range u.list { + wg.Add(1) + go func(f *FileUpload) { + defer wg.Done() + f.Open(tmpDir) + }(f) + } + + wg.Wait() + log.Print(u.list) + return nil +} + +// Clear deletes all temporary files. +func (u *Uploads) Clear() { + +} + +// parse incoming dataTree request into JSON (including multipart form dataTree) +func parseUploads(r *http.Request) (*Uploads, error) { + u := &Uploads{ + tree: make(fileTree), + list: make([]*FileUpload, 0), + } + + for k, v := range r.MultipartForm.File { + files := make([]*FileUpload, 0, len(v)) + for _, f := range v { + files = append(files, wrapUpload(f)) + } + + u.list = append(u.list, files...) + u.tree.push(k, files) + } + + return u, nil +} @@ -6,7 +6,7 @@ type Payload struct { // Context represent payload context, might be omitted Context []byte - // Body contains binary payload to be processed by worker + // body contains binary payload to be processed by worker Body []byte } diff --git a/psr7/files.go b/psr7/files.go deleted file mode 100644 index 31ddfec8..00000000 --- a/psr7/files.go +++ /dev/null @@ -1,58 +0,0 @@ -package psr7 - -import ( - "mime/multipart" - "strings" - "github.com/sirupsen/logrus" -) - -type fileData map[string]interface{} - -type FileUpload struct { - Name string `json:"name"` - MimeType string `json:"mimetype"` -} - -func (d fileData) push(k string, v []*multipart.FileHeader) { - if len(v) == 0 { - // doing nothing - return - } - - chunks := make([]string, 0) - for _, chunk := range strings.Split(k, "[") { - chunks = append(chunks, strings.Trim(chunk, "]")) - } - - d.pushChunk(chunks, v) -} - -func (d fileData) pushChunk(k []string, v []*multipart.FileHeader) { - logrus.Print(v) - if len(v) == 0 || v[0] == nil { - return - } - - head := k[0] - tail := k[1:] - if len(k) == 1 { - d[head] = FileUpload{ - Name: v[0].Filename, - MimeType: v[0].Header.Get("Content-Type"), - } - return - } - - // unnamed array - if len(tail) == 1 && tail[0] == "" { - d[head] = v - return - } - - if p, ok := d[head]; !ok { - d[head] = make(fileData) - d[head].(fileData).pushChunk(tail, v) - } else { - p.(fileData).pushChunk(tail, v) - } -} diff --git a/psr7/post.go b/psr7/post.go deleted file mode 100644 index 30af7e3a..00000000 --- a/psr7/post.go +++ /dev/null @@ -1,45 +0,0 @@ -package psr7 - -import "strings" - -type postData map[string]interface{} - -func (d postData) push(k string, v []string) { - if len(v) == 0 { - // doing nothing - return - } - - chunks := make([]string, 0) - for _, chunk := range strings.Split(k, "[") { - chunks = append(chunks, strings.Trim(chunk, "]")) - } - - d.pushChunk(chunks, v) -} - -func (d postData) pushChunk(k []string, v []string) { - if len(v) == 0 || v[0] == "" { - return - } - - head := k[0] - tail := k[1:] - if len(k) == 1 { - d[head] = v[0] - return - } - - // unnamed array - if len(tail) == 1 && tail[0] == "" { - d[head] = v - return - } - - if p, ok := d[head]; !ok { - d[head] = make(postData) - d[head].(postData).pushChunk(tail, v) - } else { - p.(postData).pushChunk(tail, v) - } -} diff --git a/psr7/request.go b/psr7/request.go deleted file mode 100644 index 41fbb4bf..00000000 --- a/psr7/request.go +++ /dev/null @@ -1,127 +0,0 @@ -package psr7 - -import ( - "net/http" - "fmt" - "encoding/json" - "github.com/spiral/roadrunner" - "github.com/sirupsen/logrus" - "strings" - "io/ioutil" -) - -type Request struct { - Protocol string `json:"protocol"` - Uri string `json:"uri"` - Method string `json:"method"` - Headers http.Header `json:"headers"` - Cookies map[string]string `json:"cookies"` - RawQuery string `json:"rawQuery"` - Uploads fileData `json:"fileUploads"` - ParsedBody bool `json:"parsedBody"` - - // buffers - postData postData - body []byte -} - -func ParseRequest(r *http.Request) (req *Request, err error) { - req = &Request{ - Protocol: r.Proto, - Uri: fmt.Sprintf("%s%s", r.Host, r.URL.String()), - Method: r.Method, - Headers: r.Header, - Cookies: make(map[string]string), - RawQuery: r.URL.RawQuery, - } - - for _, c := range r.Cookies() { - req.Cookies[c.Name] = c.Value - } - - if req.HasBody() { - r.ParseMultipartForm(32 << 20) - - if req.postData, err = parseData(r); err != nil { - return nil, err - } - - if req.Uploads, err = parseFiles(r); err != nil { - return nil, err - } - - if req.Uploads != nil { - logrus.Debug("opening files") - } - req.ParsedBody = true - } else { - req.body, _ = ioutil.ReadAll(r.Body) - } - - return req, nil -} - -func (r *Request) Payload() *roadrunner.Payload { - ctx, err := json.Marshal(r) - if err != nil { - panic(err) //todo: change it - } - - var body []byte - if r.ParsedBody { - // todo: non parseble payloads - body, err = json.Marshal(r.postData) - if err != nil { - panic(err) //todo: change it - } - } else { - body = r.body - } - - return &roadrunner.Payload{Context: ctx, Body: body} -} - -func (r *Request) Close() { - if r.Uploads != nil { - - } -} - -// HasBody returns true if request might include POST data or file uploads. -func (r *Request) HasBody() bool { - if r.Method != "POST" && r.Method != "PUT" && r.Method != "PATCH" { - return false - } - - contentType := r.Headers.Get("content-type") - - if strings.Contains(contentType, "multipart/form-data") { - return true - } - - if contentType == "application/x-www-form-urlencoded" { - return true - } - - return false -} - -// parse incoming data request into JSON (including multipart form data) -func parseData(r *http.Request) (postData, error) { - data := make(postData) - for k, v := range r.MultipartForm.Value { - data.push(k, v) - } - - return data, nil -} - -// parse incoming data request into JSON (including multipart form data) -func parseFiles(r *http.Request) (fileData, error) { - data := make(fileData) - for k, v := range r.MultipartForm.File { - data.push(k, v) - } - - return data, nil -} diff --git a/psr7/response.go b/psr7/response.go deleted file mode 100644 index 6c5f3e17..00000000 --- a/psr7/response.go +++ /dev/null @@ -1,34 +0,0 @@ -package psr7 - -import ( - "net/http" - "github.com/sirupsen/logrus" -) - -type Response struct { - Status int `json:"status"` - Headers map[string][]string `json:"headers"` -} - -func (r *Response) Write(w http.ResponseWriter) { - push := make([]string, 0) - for k, v := range r.Headers { - for _, h := range v { - if k == "http2-push" { - push = append(push, h) - } else { - w.Header().Add(k, h) - } - } - } - - if p, ok := w.(http.Pusher); ok { - logrus.Info("PUSH SUPPORTED") - for _, f := range push { - logrus.Info("pushing HTTP2 file ", f) - p.Push(f, nil) - } - } - - w.WriteHeader(r.Status) -} diff --git a/psr7/server.go b/psr7/server.go deleted file mode 100644 index a1c26146..00000000 --- a/psr7/server.go +++ /dev/null @@ -1,129 +0,0 @@ -package psr7 - -import ( - "github.com/spiral/roadrunner" - "net/http" - "strings" - "path" - "github.com/sirupsen/logrus" - "os" - "path/filepath" - "encoding/json" -) - -var ( - excludeFiles = []string{".php", ".htaccess"} -) - -// Configures http rr -type Config struct { - // ServeStatic enables static file serving from desired root directory. - ServeStatic bool - - // Root directory, required when ServeStatic set to true. - Root string -} - -// Server serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, -// parsed files and query, payload will include parsed form data (if any) - todo: do we need to do that?. -type Server struct { - cfg Config - root http.Dir - rr *roadrunner.Server -} - -// NewServer returns new instance of Server PSR7 server. -func NewServer(cfg Config, server *roadrunner.Server) *Server { - h := &Server{cfg: cfg, rr: server} - if cfg.ServeStatic { - h.root = http.Dir(h.cfg.Root) - } - - return h -} - -// ServeHTTP serve using PSR-7 requests passed to underlying application. -func (svr *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () { - if svr.cfg.ServeStatic && svr.serveStatic(w, r) { - // server always attempt to serve static files first - return - } - - req, err := ParseRequest(r) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) //todo: better errors - return - } - defer req.Close() - - rsp, err := svr.rr.Exec(req.Payload()) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) //todo: better errors - return - } - - resp := &Response{} - if err = json.Unmarshal(rsp.Context, resp); err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) //todo: better errors - return - } - - resp.Write(w) - w.Write(rsp.Body) -} - -// serveStatic attempts to serve static file and returns true in case of success, will return false in case if file not -// found, not allowed or on read error. -func (svr *Server) serveStatic(w http.ResponseWriter, r *http.Request) bool { - fpath := r.URL.Path - if !strings.HasPrefix(fpath, "/") { - fpath = "/" + fpath - } - fpath = path.Clean(fpath) - - if svr.excluded(fpath) { - logrus.Warningf("attempt to access forbidden file %s", fpath) - return false - } - - f, err := svr.root.Open(fpath) - if err != nil { - if !os.IsNotExist(err) { - // rr or access error - logrus.Error(err) - } - - return false - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - // rr error - logrus.Error(err) - return false - } - - if d.IsDir() { - // we are not serving directories - return false - } - - http.ServeContent(w, r, d.Name(), d.ModTime(), f) - return true -} - -// excluded returns true if file has forbidden extension. -func (svr *Server) excluded(path string) bool { - ext := strings.ToLower(filepath.Ext(path)) - for _, exl := range excludeFiles { - if ext == exl { - return true - } - } - - return false -} |