feat: add file upload support for file handler (issue #854)
Add PUT method support to the file:// handler, allowing file uploads via curl -T. Upload is opt-in, disabled by default — enable with ?put=true metadata flag. Includes path traversal protection.
This commit is contained in:
+62
-1
@@ -4,9 +4,14 @@ package file
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -52,7 +57,18 @@ func (h *fileHandler) Init(md md.Metadata) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.handler = http.FileServer(http.Dir(h.md.dir))
|
fs := http.FileServer(http.Dir(h.md.dir))
|
||||||
|
h.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
if !h.md.put {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.handlePUT(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
h.server = &http.Server{
|
h.server = &http.Server{
|
||||||
Handler: http.HandlerFunc(h.handleFunc),
|
Handler: http.HandlerFunc(h.handleFunc),
|
||||||
}
|
}
|
||||||
@@ -101,6 +117,51 @@ func (h *fileHandler) Close() error {
|
|||||||
return h.server.Close()
|
return h.server.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *fileHandler) handlePUT(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Reject path traversal attempts.
|
||||||
|
target := path.Clean(r.URL.Path)
|
||||||
|
if strings.Contains(target, "..") || !strings.HasPrefix(target, "/") {
|
||||||
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := filepath.Join(h.md.dir, filepath.FromSlash(target))
|
||||||
|
// Verify the resolved path is within the base directory.
|
||||||
|
base, err := filepath.Abs(h.md.dir)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
absDest, err := filepath.Abs(dest)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(absDest, base) {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(absDest), 0755); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(absDest)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(f, r.Body); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) {
|
func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
dir string
|
dir string
|
||||||
|
put bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *fileHandler) parseMetadata(md mdata.Metadata) (err error) {
|
func (h *fileHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||||
h.md.dir = mdutil.GetString(md, "file.dir", "dir")
|
h.md.dir = mdutil.GetString(md, "file.dir", "dir")
|
||||||
|
h.md.put = mdutil.GetBool(md, "file.put", "put")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user