1287 lines
34 KiB
Go
1287 lines
34 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"mime/multipart"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"chfs-ng/internal/auth"
|
|
"chfs-ng/internal/opslog"
|
|
"chfs-ng/internal/storage"
|
|
"github.com/anthonynsimon/bild/transform"
|
|
"github.com/emersion/go-webdav"
|
|
)
|
|
|
|
type Server struct {
|
|
Store storage.Storage
|
|
Auth *auth.Manager
|
|
Sessions map[string]*auth.Account
|
|
SessionExpiry map[string]time.Time
|
|
SessionMu sync.RWMutex
|
|
SessionTimeout int
|
|
WebDAVRoot string
|
|
WebDAVFS webdav.FileSystem
|
|
Allow string
|
|
MultiRoot bool
|
|
RemoveMode int
|
|
ImagePreview bool
|
|
Log *opslog.Logger
|
|
}
|
|
type fileInfo struct {
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
Modified string `json:"modified"`
|
|
Dir bool `json:"dir"`
|
|
Mask string `json:"mask"`
|
|
GuestMask string `json:"guestMask"`
|
|
HasSubDir bool `json:"hasSubDir"`
|
|
Icon string `json:"icon"`
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/webdav/", s.webdav)
|
|
mux.HandleFunc("/webdav", s.webdav)
|
|
mux.HandleFunc("/chfs/files", s.files)
|
|
mux.HandleFunc("/chfs/newdir", s.newdir)
|
|
mux.HandleFunc("/chfs/rename", s.rename)
|
|
mux.HandleFunc("/chfs/rmfiles", s.remove)
|
|
mux.HandleFunc("/chfs/exist", s.exist)
|
|
mux.HandleFunc("/chfs/session", s.session)
|
|
mux.HandleFunc("/chfs/newtext", s.newtext)
|
|
mux.HandleFunc("/chfs/updatetext", s.updatetext)
|
|
mux.HandleFunc("/chfs/upload", s.upload)
|
|
mux.HandleFunc("/chfs/upload2", s.upload2)
|
|
mux.HandleFunc("/chfs/search", s.search)
|
|
mux.HandleFunc("/chfs/downloaddir/", s.downloadDir)
|
|
mux.HandleFunc("/chfs/shared", s.shared)
|
|
mux.HandleFunc("/chfs/shared/", s.shared)
|
|
mux.HandleFunc("/chfs/preview", s.preview)
|
|
mux.HandleFunc("/", s.fileHandler)
|
|
return s.ipFilter(mux)
|
|
}
|
|
func account(r *http.Request, s *Server) *auth.Account {
|
|
c, err := r.Cookie("JWT")
|
|
if err != nil {
|
|
return s.Auth.Guest
|
|
}
|
|
username, err := auth.GetAccFromToken(c.Value)
|
|
if err != nil {
|
|
return s.Auth.Guest
|
|
}
|
|
if a, ok := s.Auth.Accounts[username]; ok {
|
|
return a
|
|
}
|
|
return s.Auth.Guest
|
|
}
|
|
|
|
func (s *Server) logOp(r *http.Request, action string) {
|
|
if s.Log == nil {
|
|
return
|
|
}
|
|
a := account(r, s)
|
|
user := ""
|
|
if a != nil {
|
|
user = a.Username
|
|
}
|
|
_ = s.Log.Write(clientIPString(r), user, action)
|
|
}
|
|
|
|
func clientIPString(r *http.Request) string {
|
|
ip := clientIP(r)
|
|
if ip == nil {
|
|
return "-"
|
|
}
|
|
return ip.String()
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
|
}
|
|
func qpath(r *http.Request) string {
|
|
return strings.Trim(strings.ReplaceAll(r.URL.Query().Get("filepath"), "\\", "/"), "/")
|
|
}
|
|
func permString(p auth.PermMask) string {
|
|
if p.HasDelete() {
|
|
return "RWD"
|
|
}
|
|
if p.HasWrite() {
|
|
return "RW"
|
|
}
|
|
if p.HasRead() {
|
|
return "R"
|
|
}
|
|
return ""
|
|
}
|
|
func iconFor(fi os.FileInfo) string {
|
|
if fi.IsDir() {
|
|
return "asset/icons/folder.svg"
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(fi.Name()))
|
|
switch ext {
|
|
case ".txt", ".md", ".log", ".json", ".xml", ".yaml", ".yml", ".ini", ".conf", ".cfg":
|
|
return "asset/icons/txt.svg"
|
|
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg":
|
|
return "asset/icons/image.svg"
|
|
case ".mp3", ".wav", ".flac", ".aac", ".m4a", ".ogg":
|
|
return "asset/icons/audio.svg"
|
|
case ".mp4", ".mkv", ".avi", ".mov", ".webm":
|
|
return "asset/icons/video.svg"
|
|
case ".zip", ".rar", ".7z", ".tar", ".gz":
|
|
return "asset/icons/archive.svg"
|
|
case ".pdf":
|
|
return "asset/icons/pdf.svg"
|
|
default:
|
|
return "asset/icons/unknown.svg"
|
|
}
|
|
}
|
|
|
|
func originalTime(t time.Time) string { return t.Format("2006-01-02 15:04:05") }
|
|
|
|
func writeCreated(w http.ResponseWriter) { w.WriteHeader(http.StatusCreated) }
|
|
|
|
func (s *Server) files(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
p := qpath(r)
|
|
m := a.MaskFor(p)
|
|
if !m.HasRead() {
|
|
// CHFS keeps the files endpoint HTTP-successful for a denied directory
|
|
// and returns an empty listing with currentMask="".
|
|
writeJSON(w, map[string]any{"files": []fileInfo{}, "currentMask": "", "virtualRoot": 0})
|
|
return
|
|
}
|
|
list, e := s.Store.List(p)
|
|
if e != nil {
|
|
http.Error(w, e.Error(), 404)
|
|
return
|
|
}
|
|
out := make([]fileInfo, 0, len(list))
|
|
for _, fi := range list {
|
|
fp := path.Join(p, fi.Name())
|
|
// A rule.none entry is not merely read-less metadata: CHFS hides the
|
|
// entry from directory listings because the directory itself is denied.
|
|
if !a.MaskForEntry(fp).HasRead() {
|
|
continue
|
|
}
|
|
sub := false
|
|
if fi.IsDir() {
|
|
if xs, e := s.Store.List(fp); e == nil {
|
|
for _, child := range xs {
|
|
if child.IsDir() {
|
|
sub = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out = append(out, fileInfo{Name: fi.Name(), Size: fi.Size(), Modified: originalTime(fi.ModTime()), Dir: fi.IsDir(), Mask: permString(a.MaskForEntry(fp)), GuestMask: permString(s.Auth.Guest.MaskForEntry(fp)), HasSubDir: sub, Icon: iconFor(fi)})
|
|
}
|
|
virtual := 0
|
|
if p == "" && s.MultiRoot {
|
|
virtual = 1
|
|
}
|
|
writeJSON(w, map[string]any{"files": out, "currentMask": permString(m), "virtualRoot": virtual})
|
|
}
|
|
func (s *Server) newdir(w http.ResponseWriter, r *http.Request) {
|
|
_ = r.ParseMultipartForm(16 << 20)
|
|
_ = r.ParseForm()
|
|
p := qpath(r)
|
|
if p == "" {
|
|
p = strings.Trim(strings.ReplaceAll(r.FormValue("filepath"), "\\", "/"), "/")
|
|
}
|
|
if p == "" {
|
|
p = strings.Trim(r.FormValue("name"), "/")
|
|
}
|
|
if p == "" {
|
|
p = strings.Trim(r.FormValue("new"), "/")
|
|
}
|
|
if p == "" {
|
|
http.Error(w, "filepath required", 400)
|
|
return
|
|
}
|
|
a := account(r, s)
|
|
if !a.MaskFor(p).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
if e := s.Store.MkdirAll(p); e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
s.logOp(r, fmt.Sprintf("created directory '%s'", p))
|
|
writeCreated(w)
|
|
}
|
|
func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
old := strings.Trim(r.FormValue("old"), "/")
|
|
neu := strings.Trim(r.FormValue("new"), "/")
|
|
if old == "" || neu == "" || !a.MaskFor(old).HasWrite() || !a.MaskFor(path.Dir(neu)).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
if e := s.Store.Rename(old, neu); e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
s.logOp(r, fmt.Sprintf("renamed '%s' to '%s'", old, neu))
|
|
writeCreated(w)
|
|
}
|
|
func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
_ = r.ParseMultipartForm(16 << 20)
|
|
_ = r.ParseForm()
|
|
items := r.Form["filepath"]
|
|
if len(items) == 0 {
|
|
items = r.Form["path"]
|
|
}
|
|
for _, raw := range items {
|
|
p := strings.Trim(strings.ReplaceAll(raw, "\\", "/"), "/")
|
|
if p == "" || !a.MaskFor(p).HasDelete() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
if e := s.removePath(p); e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
s.logOp(r, fmt.Sprintf("removed '%s'", strings.Trim(p, "/")))
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
func (s *Server) removePath(p string) error {
|
|
mode := s.RemoveMode
|
|
if mode == 0 {
|
|
mode = 1
|
|
}
|
|
t, ok := s.Store.(storage.Trashable)
|
|
switch mode {
|
|
case 1:
|
|
if !ok {
|
|
return fmt.Errorf("storage does not support system trash")
|
|
}
|
|
return t.Trash(p)
|
|
case 2:
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.MoveToTrash(p, filepath.Join(home, ".chfs_trashbin"))
|
|
case 3:
|
|
return s.Store.Remove(p)
|
|
default:
|
|
return fmt.Errorf("invalid file.remove mode: %d", mode)
|
|
}
|
|
}
|
|
|
|
func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
|
|
p := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/")
|
|
_, e := s.Store.Stat(p)
|
|
if e != nil {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodDelete {
|
|
jwtValue, _ := r.Cookie("JWT")
|
|
userValue, _ := r.Cookie("user")
|
|
jwt := ""
|
|
user := ""
|
|
if jwtValue != nil {
|
|
jwt = jwtValue.Value
|
|
}
|
|
if userValue != nil {
|
|
user = userValue.Value
|
|
}
|
|
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: jwt, Path: "/", MaxAge: -1})
|
|
http.SetCookie(w, &http.Cookie{Name: "user", Value: user, Path: "/", MaxAge: -1})
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
user, pwd := r.FormValue("user"), r.FormValue("pwd")
|
|
if user == "" && pwd == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
|
return
|
|
}
|
|
a := s.Auth.Authenticate(user, pwd)
|
|
if a == nil {
|
|
writeJSONError(w, http.StatusNotFound, "user or password is wrong")
|
|
return
|
|
}
|
|
token, err := auth.NewToken(a.Username)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "failed to create token")
|
|
return
|
|
}
|
|
expires := time.Now().Add(365 * 24 * time.Hour)
|
|
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: token, Path: "/", Expires: expires, HttpOnly: true})
|
|
http.SetCookie(w, &http.Cookie{Name: "user", Value: a.Username, Path: "/", Expires: expires})
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
dir := strings.Trim(strings.ReplaceAll(r.FormValue("filepath"), "\\", "/"), "/")
|
|
filename := strings.Trim(r.FormValue("title"), "/")
|
|
if filename == "" {
|
|
filename = strings.Trim(r.FormValue("filename"), "/")
|
|
}
|
|
if filename == "" {
|
|
http.Error(w, "title required", 400)
|
|
return
|
|
}
|
|
p := path.Join(dir, filename)
|
|
if !a.MaskFor(p).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
f, e := s.Store.Create(p)
|
|
if e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
io.WriteString(f, r.FormValue("text"))
|
|
s.logOp(r, fmt.Sprintf("create a text file:'%s'", p))
|
|
writeCreated(w)
|
|
}
|
|
func (s *Server) updatetext(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
dir := strings.Trim(r.FormValue("filepath"), "/")
|
|
name := strings.Trim(r.FormValue("filename"), "/")
|
|
p := path.Join(dir, name)
|
|
if name == "" || !a.MaskFor(p).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
f, e := s.Store.Create(p)
|
|
if e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
if _, e = io.WriteString(f, r.FormValue("text")); e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
s.logOp(r, fmt.Sprintf("update text file:'%s'", p))
|
|
writeCreated(w)
|
|
}
|
|
func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|
if e := r.ParseMultipartForm(64 << 20); e != nil {
|
|
http.Error(w, e.Error(), 400)
|
|
return
|
|
}
|
|
a := account(r, s)
|
|
dir := strings.Trim(r.FormValue("folder"), "/")
|
|
if !a.MaskFor(dir).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
var saved []string
|
|
for _, hs := range r.MultipartForm.File {
|
|
for _, h := range hs {
|
|
name := filepath.ToSlash(h.Filename)
|
|
if name == "" || strings.HasPrefix(name, "/") || strings.Contains(name, "../") || name == ".." {
|
|
http.Error(w, "invalid filename", 400)
|
|
return
|
|
}
|
|
if e := savePart(s.Store, path.Join(dir, name), h); e != nil {
|
|
http.Error(w, e.Error(), 500)
|
|
return
|
|
}
|
|
saved = append(saved, h.Filename)
|
|
}
|
|
}
|
|
for _, name := range saved {
|
|
s.logOp(r, fmt.Sprintf("uploaded '%s'", filepath.Base(name)))
|
|
}
|
|
writeCreated(w)
|
|
}
|
|
func savePart(st storage.Storage, target string, h *multipart.FileHeader) error {
|
|
in, e := h.Open()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
defer in.Close()
|
|
f, e := st.Create(target)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
defer f.Close()
|
|
_, e = io.Copy(f, in)
|
|
return e
|
|
}
|
|
|
|
func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
dir := strings.Trim(r.URL.Query().Get("folder"), "/")
|
|
if !a.MaskFor(dir).HasWrite() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
if err := r.ParseMultipartForm(128 << 20); err != nil {
|
|
http.Error(w, err.Error(), 400)
|
|
return
|
|
}
|
|
var saved []string
|
|
for _, hs := range r.MultipartForm.File {
|
|
for _, h := range hs {
|
|
name := filepath.ToSlash(h.Filename)
|
|
name = strings.TrimPrefix(name, "/")
|
|
if name == "" || strings.Contains(name, "../") || name == ".." {
|
|
http.Error(w, "invalid filename", 400)
|
|
return
|
|
}
|
|
if err := savePart(s.Store, path.Join(dir, name), h); err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
saved = append(saved, path.Join(dir, name))
|
|
}
|
|
}
|
|
writeCreated(w)
|
|
}
|
|
|
|
type searchFileInfo struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
Modified string `json:"modified"`
|
|
Dir bool `json:"dir"`
|
|
Mask string `json:"mask"`
|
|
GuestMask string `json:"guestMask"`
|
|
HasSubDir bool `json:"hasSubDir"`
|
|
Icon string `json:"icon"`
|
|
}
|
|
|
|
func (s *Server) search(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
rootMask := a.MaskFor("")
|
|
if !rootMask.HasRead() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
needle := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("str")))
|
|
out := make([]searchFileInfo, 0)
|
|
var walk func(string) error
|
|
walk = func(dir string) error {
|
|
items, err := s.Store.List(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, fi := range items {
|
|
p := path.Join(dir, fi.Name())
|
|
if !a.MaskFor(p).HasRead() {
|
|
continue
|
|
}
|
|
if needle == "" || strings.Contains(strings.ToLower(fi.Name()), needle) {
|
|
out = append(out, searchFileInfo{Name: fi.Name(), Path: "/" + strings.Trim(p, "/"), Size: fi.Size(), Modified: originalTime(fi.ModTime()), Dir: fi.IsDir(), Mask: permString(a.MaskForEntry(p)), GuestMask: permString(s.Auth.Guest.MaskForEntry(p)), HasSubDir: false, Icon: iconFor(fi)})
|
|
}
|
|
if fi.IsDir() {
|
|
if err := walk(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := walk(""); err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"virtualRoot": 0, "currentMask": permString(rootMask), "files": out})
|
|
}
|
|
|
|
func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/chfs/shared" {
|
|
location := "shared/"
|
|
if r.URL.RawQuery != "" {
|
|
location += "?" + r.URL.RawQuery
|
|
}
|
|
http.Redirect(w, r, location, http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
p := strings.Trim(strings.TrimPrefix(r.URL.Path, "/chfs/shared/"), "/")
|
|
fi, err := s.Store.Stat(p)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if fi.IsDir() {
|
|
items, err := s.Store.List(p)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Disposition", `attachment;filename="shared"`)
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_, _ = io.WriteString(w, "<pre>\n")
|
|
for _, item := range items {
|
|
name := item.Name()
|
|
href := url.PathEscape(name)
|
|
if item.IsDir() {
|
|
href += "/"
|
|
}
|
|
_, _ = fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", href, html.EscapeString(name)+func() string {
|
|
if item.IsDir() {
|
|
return "/"
|
|
}
|
|
return ""
|
|
}())
|
|
}
|
|
_, _ = io.WriteString(w, "</pre>\n")
|
|
return
|
|
}
|
|
f, err := s.Store.Open(p)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, fi.Name()))
|
|
http.ServeContent(w, r, fi.Name(), fi.ModTime(), f)
|
|
}
|
|
|
|
func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
|
|
file := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/")
|
|
if file == "" {
|
|
http.Error(w, "file required", 400)
|
|
return
|
|
}
|
|
if !account(r, s).MaskFor(file).HasRead() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
if !s.ImagePreview {
|
|
writeJSONError(w, http.StatusInternalServerError, "Open image file fail!")
|
|
return
|
|
}
|
|
f, err := s.Store.Open(file)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "Open image file fail!")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
src, _, err := image.Decode(f)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "Open image file fail!")
|
|
return
|
|
}
|
|
thumb := transform.Resize(src, 64, 64, transform.Linear)
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, thumb); err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "Open image file fail!")
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "max-age=2592000")
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(buf.Bytes())
|
|
}
|
|
|
|
func (s *Server) fileHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/" || strings.HasPrefix(r.URL.Path, "/asset/") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/chfs/") || r.URL.Path == "/webdav" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
p := strings.Trim(strings.ReplaceAll(r.URL.Path, "\\", "/"), "/")
|
|
if p == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
s.serveFile(w, r, p, false)
|
|
}
|
|
|
|
func (s *Server) serveFile(w http.ResponseWriter, r *http.Request, p string, attachment bool) {
|
|
a := account(r, s)
|
|
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
|
if !a.MaskFor(p).HasRead() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
fi, err := s.Store.Stat(p)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if fi.IsDir() {
|
|
http.Error(w, "directory", 400)
|
|
return
|
|
}
|
|
f, err := s.Store.Open(p)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
if attachment {
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", fi.Name()))
|
|
}
|
|
s.logOp(r, fmt.Sprintf("download '%s'", p))
|
|
http.ServeContent(w, r, fi.Name(), fi.ModTime(), f)
|
|
}
|
|
|
|
func (s *Server) downloadDir(w http.ResponseWriter, r *http.Request) {
|
|
a := account(r, s)
|
|
p := strings.TrimPrefix(r.URL.Path, "/chfs/downloaddir/")
|
|
p = strings.Trim(p, "/")
|
|
if !a.MaskFor(p).HasRead() {
|
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
|
return
|
|
}
|
|
fi, err := s.Store.Stat(p)
|
|
if err != nil || !fi.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
name := fi.Name()
|
|
if name == "." || name == "" {
|
|
name = "download"
|
|
}
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q.zip", name))
|
|
zw := zip.NewWriter(w)
|
|
defer zw.Close()
|
|
var walk func(string, string) error
|
|
walk = func(dir, prefix string) error {
|
|
items, err := s.Store.List(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range items {
|
|
p2 := path.Join(dir, item.Name())
|
|
rel := path.Join(prefix, item.Name())
|
|
if !a.MaskFor(p2).HasRead() {
|
|
continue
|
|
}
|
|
if item.IsDir() {
|
|
if err := walk(p2, rel); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
h, err := zw.Create(rel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := s.Store.Open(p2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(h, f)
|
|
f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
s.logOp(r, fmt.Sprintf("downloaded '/%s'", strings.Trim(p, "/")))
|
|
if err := walk(p, ""); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *Server) webdav(w http.ResponseWriter, r *http.Request) {
|
|
if s.WebDAVRoot == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
vp := strings.Trim(strings.TrimPrefix(r.URL.Path, "/webdav"), "/")
|
|
old := r.URL.Path
|
|
r.URL.Path = "/" + vp
|
|
defer func() { r.URL.Path = old }()
|
|
inner := s.WebDAVFS
|
|
if inner == nil {
|
|
inner = &webdavStorageFS{store: s.Store, removeMode: s.RemoveMode}
|
|
}
|
|
fs := &webdavAuthFS{inner: inner, account: account(r, s)}
|
|
(&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r)
|
|
}
|
|
|
|
type webdavStorageFS struct {
|
|
store storage.Storage
|
|
removeMode int
|
|
}
|
|
|
|
func davStoragePath(name string) string {
|
|
p := strings.Trim(strings.ReplaceAll(name, "\\", "/"), "/")
|
|
p = strings.TrimPrefix(p, "webdav/")
|
|
p = strings.TrimPrefix(p, "webdav")
|
|
return strings.Trim(p, "/")
|
|
}
|
|
|
|
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
|
|
p := davStoragePath(name)
|
|
fi, err := f.store.Stat(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &webdav.FileInfo{Path: "/" + p, Size: fi.Size(), ModTime: fi.ModTime(), IsDir: fi.IsDir()}, nil
|
|
}
|
|
|
|
func (f *webdavStorageFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
|
p := davStoragePath(name)
|
|
fi, err := f.store.Stat(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fi.IsDir() {
|
|
return nil, fmt.Errorf("is a directory")
|
|
}
|
|
return f.store.Open(p)
|
|
}
|
|
|
|
func (f *webdavStorageFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
|
|
return f.info(name)
|
|
}
|
|
|
|
func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
|
root := davStoragePath(name)
|
|
var out []webdav.FileInfo
|
|
var walk func(string) error
|
|
walk = func(dir string) error {
|
|
items, err := f.store.List(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range items {
|
|
p := path.Join(dir, item.Name())
|
|
out = append(out, webdav.FileInfo{Path: "/" + p, Size: item.Size(), ModTime: item.ModTime(), IsDir: item.IsDir()})
|
|
if recursive && item.IsDir() {
|
|
if err := walk(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := walk(root); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
|
p := davStoragePath(name)
|
|
_, statErr := f.store.Stat(p)
|
|
if statErr != nil && !os.IsNotExist(statErr) {
|
|
return nil, false, statErr
|
|
}
|
|
file, err := f.store.Create(p)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
_, copyErr := io.Copy(file, body)
|
|
closeErr := file.Close()
|
|
body.Close()
|
|
if copyErr != nil {
|
|
return nil, false, copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return nil, false, closeErr
|
|
}
|
|
fi, err := f.info(p)
|
|
return fi, true, err
|
|
}
|
|
|
|
func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
|
p := davStoragePath(name)
|
|
if p == "" {
|
|
return fmt.Errorf("cannot remove storage root")
|
|
}
|
|
if f.removeMode == 1 {
|
|
if t, ok := f.store.(storage.Trashable); ok {
|
|
return t.Trash(p)
|
|
}
|
|
}
|
|
if f.removeMode == 2 {
|
|
if t, ok := f.store.(storage.Trashable); ok {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.MoveToTrash(p, filepath.Join(home, ".chfs_trashbin"))
|
|
}
|
|
}
|
|
return f.store.Remove(p)
|
|
}
|
|
|
|
func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
|
|
p := davStoragePath(name)
|
|
if p == "" {
|
|
return fmt.Errorf("cannot create storage root")
|
|
}
|
|
return f.store.MkdirAll(p)
|
|
}
|
|
|
|
func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
|
src := davStoragePath(name)
|
|
dst := davStoragePath(dest)
|
|
if m, ok := f.store.(storage.Multi); ok {
|
|
srcRoot, _, srcOK := m.FindRoot(src)
|
|
dstRoot, _, dstOK := m.FindRoot(dst)
|
|
if srcOK && dstOK && srcRoot != dstRoot {
|
|
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported"))
|
|
}
|
|
}
|
|
info, err := f.store.Stat(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if info.IsDir() {
|
|
if err := f.store.MkdirAll(dst); err != nil {
|
|
return false, err
|
|
}
|
|
items, err := f.store.List(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, item := range items {
|
|
created, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
_ = created
|
|
}
|
|
return true, nil
|
|
}
|
|
in, err := f.store.Open(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
out, err := f.store.Create(dst)
|
|
if err != nil {
|
|
in.Close()
|
|
return false, err
|
|
}
|
|
_, copyErr := io.Copy(out, in)
|
|
in.Close()
|
|
closeErr := out.Close()
|
|
if copyErr != nil {
|
|
return false, copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return false, closeErr
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
|
src := davStoragePath(name)
|
|
dst := davStoragePath(dest)
|
|
if opts != nil && opts.NoOverwrite {
|
|
if _, err := f.store.Stat(dst); err == nil {
|
|
return false, fmt.Errorf("destination exists")
|
|
}
|
|
}
|
|
if m, ok := f.store.(storage.Multi); ok {
|
|
srcRoot, _, srcOK := m.FindRoot(src)
|
|
dstRoot, _, dstOK := m.FindRoot(dst)
|
|
if srcOK && dstOK && srcRoot != dstRoot {
|
|
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported"))
|
|
}
|
|
}
|
|
if err := f.store.Rename(src, dst); err != nil {
|
|
return false, webdav.NewHTTPError(http.StatusForbidden, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type webdavMultiFS struct {
|
|
roots map[string]webdav.FileSystem
|
|
names []string
|
|
}
|
|
|
|
func newWebDAVMultiFS(m storage.Multi) *webdavMultiFS {
|
|
fs := &webdavMultiFS{roots: make(map[string]webdav.FileSystem), names: append([]string(nil), m.Names...)}
|
|
for i, name := range m.Names {
|
|
if i < len(m.Roots) {
|
|
fs.roots[name] = webdav.LocalFileSystem(m.Roots[i].Root)
|
|
}
|
|
}
|
|
return fs
|
|
}
|
|
func splitDAVPath(name string) (string, string) {
|
|
p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/")
|
|
p = strings.TrimPrefix(p, "webdav/")
|
|
p = strings.TrimPrefix(p, "webdav")
|
|
if p == "" {
|
|
return "", ""
|
|
}
|
|
parts := strings.SplitN(p, "/", 2)
|
|
if len(parts) == 1 {
|
|
return parts[0], ""
|
|
}
|
|
return parts[0], parts[1]
|
|
}
|
|
func (f *webdavMultiFS) root(name string) (webdav.FileSystem, string, error) {
|
|
r, rel := splitDAVPath(name)
|
|
if r == "" {
|
|
return nil, "", nil
|
|
}
|
|
fs, ok := f.roots[r]
|
|
if !ok {
|
|
return nil, "", webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("root not found"))
|
|
}
|
|
return fs, rel, nil
|
|
}
|
|
func (f *webdavMultiFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fs == nil {
|
|
return nil, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("virtual root is not a file"))
|
|
}
|
|
return fs.Open(ctx, "/"+rel)
|
|
}
|
|
func (f *webdavMultiFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
|
|
if strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/") == "" {
|
|
return &webdav.FileInfo{Path: "/", IsDir: true}, nil
|
|
}
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rel == "" {
|
|
return &webdav.FileInfo{Path: "/" + strings.Trim(strings.SplitN(strings.Trim(name, "/"), "/", 2)[0], "/"), IsDir: true}, nil
|
|
}
|
|
fi, err := fs.Stat(ctx, "/"+rel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fi.Path = "/" + strings.Trim(name, "/")
|
|
return fi, nil
|
|
}
|
|
func (f *webdavMultiFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
|
p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/")
|
|
if p == "" {
|
|
out := make([]webdav.FileInfo, 0, len(f.names))
|
|
for _, n := range f.names {
|
|
out = append(out, webdav.FileInfo{Path: "/" + n, IsDir: true})
|
|
}
|
|
return out, nil
|
|
}
|
|
fs, rel, err := f.root(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := fs.ReadDir(ctx, "/"+rel, recursive)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, _ := splitDAVPath(p)
|
|
for i := range items {
|
|
itemPath := strings.TrimPrefix(items[i].Path, "/")
|
|
if itemPath == "" {
|
|
itemPath = filepath.Base(items[i].Path)
|
|
}
|
|
items[i].Path = "/" + path.Join(root, itemPath)
|
|
}
|
|
return items, nil
|
|
}
|
|
func (f *webdavMultiFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if fs == nil || rel == "" {
|
|
return nil, false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root"))
|
|
}
|
|
fi, created, err := fs.Create(ctx, "/"+rel, body, opts)
|
|
if err == nil {
|
|
fi.Path = "/" + strings.Trim(name, "/")
|
|
}
|
|
return fi, created, err
|
|
}
|
|
func (f *webdavMultiFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fs == nil || rel == "" {
|
|
return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot remove virtual root"))
|
|
}
|
|
return fs.RemoveAll(ctx, "/"+rel, opts)
|
|
}
|
|
func (f *webdavMultiFS) Mkdir(ctx context.Context, name string) error {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fs == nil || rel == "" {
|
|
return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root"))
|
|
}
|
|
return fs.Mkdir(ctx, "/"+rel)
|
|
}
|
|
func (f *webdavMultiFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
dfs, drel, err := f.root(dest)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if fs == nil || dfs == nil || fs != dfs {
|
|
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported"))
|
|
}
|
|
return fs.Copy(ctx, "/"+rel, "/"+drel, opts)
|
|
}
|
|
func (f *webdavMultiFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
|
fs, rel, err := f.root(name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
dfs, drel, err := f.root(dest)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if fs == nil || dfs == nil || fs != dfs {
|
|
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported"))
|
|
}
|
|
return fs.Move(ctx, "/"+rel, "/"+drel, opts)
|
|
}
|
|
|
|
type webdavAuthFS struct {
|
|
inner webdav.FileSystem
|
|
account *auth.Account
|
|
}
|
|
|
|
func webdavAuthPath(p string) string {
|
|
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
|
|
p = strings.TrimPrefix(p, "webdav/")
|
|
p = strings.TrimPrefix(p, "webdav")
|
|
return strings.Trim(p, "/")
|
|
}
|
|
|
|
func (f *webdavAuthFS) allow(path string, p auth.PermMask) error {
|
|
path = webdavAuthPath(path)
|
|
if f.account == nil || f.account.MaskFor(path)&p != p {
|
|
return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("forbidden"))
|
|
}
|
|
return nil
|
|
}
|
|
func (f *webdavAuthFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
|
if err := f.allow(name, auth.Read); err != nil {
|
|
return nil, err
|
|
}
|
|
return f.inner.Open(ctx, name)
|
|
}
|
|
func (f *webdavAuthFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
|
|
if err := f.allow(name, auth.Read); err != nil {
|
|
return nil, err
|
|
}
|
|
return f.inner.Stat(ctx, name)
|
|
}
|
|
func (f *webdavAuthFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
|
if err := f.allow(name, auth.Read); err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := f.inner.ReadDir(ctx, name, recursive)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]webdav.FileInfo, 0, len(items))
|
|
for _, item := range items {
|
|
p := strings.Trim(item.Path, "/")
|
|
if f.account.MaskFor(p).HasRead() {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
func (f *webdavAuthFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
|
parent := path.Dir(strings.Trim(name, "/"))
|
|
if parent == "." {
|
|
parent = ""
|
|
}
|
|
if err := f.allow(parent, auth.Write); err != nil {
|
|
return nil, false, err
|
|
}
|
|
return f.inner.Create(ctx, name, body, opts)
|
|
}
|
|
func (f *webdavAuthFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
|
if err := f.allow(name, auth.Delete); err != nil {
|
|
return err
|
|
}
|
|
if err := f.checkTree(ctx, name, auth.Delete); err != nil {
|
|
return err
|
|
}
|
|
return f.inner.RemoveAll(ctx, name, opts)
|
|
}
|
|
func (f *webdavAuthFS) Mkdir(ctx context.Context, name string) error {
|
|
parent := path.Dir(strings.Trim(name, "/"))
|
|
if parent == "." {
|
|
parent = ""
|
|
}
|
|
if err := f.allow(parent, auth.Write); err != nil {
|
|
return err
|
|
}
|
|
return f.inner.Mkdir(ctx, name)
|
|
}
|
|
func (f *webdavAuthFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
|
if err := f.allow(name, auth.Read); err != nil {
|
|
return false, err
|
|
}
|
|
if err := f.checkTree(ctx, name, auth.Read); err != nil {
|
|
return false, err
|
|
}
|
|
parent := path.Dir(webdavAuthPath(dest))
|
|
if parent == "." {
|
|
parent = ""
|
|
}
|
|
if err := f.allow(parent, auth.Write); err != nil {
|
|
return false, err
|
|
}
|
|
return f.inner.Copy(ctx, name, dest, opts)
|
|
}
|
|
func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
|
if err := f.allow(name, auth.Delete); err != nil {
|
|
return false, err
|
|
}
|
|
if err := f.checkTree(ctx, name, auth.Delete); err != nil {
|
|
return false, err
|
|
}
|
|
parent := path.Dir(webdavAuthPath(dest))
|
|
if parent == "." {
|
|
parent = ""
|
|
}
|
|
if err := f.allow(parent, auth.Write); err != nil {
|
|
return false, err
|
|
}
|
|
return f.inner.Move(ctx, name, dest, opts)
|
|
}
|
|
|
|
func (f *webdavAuthFS) checkTree(ctx context.Context, name string, p auth.PermMask) error {
|
|
info, err := f.inner.Stat(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir {
|
|
return nil
|
|
}
|
|
items, err := f.inner.ReadDir(ctx, name, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range items {
|
|
itemPath := strings.Trim(item.Path, "/")
|
|
if err := f.allow(itemPath, p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) ipFilter(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if s.Allow != "" && !allowedIP(s.Allow, clientIP(r)) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func clientIP(r *http.Request) net.IP {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err == nil {
|
|
return net.ParseIP(host)
|
|
}
|
|
return net.ParseIP(r.RemoteAddr)
|
|
}
|
|
|
|
func allowedIP(rule string, ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
rule = strings.TrimSpace(rule)
|
|
black := false
|
|
if strings.HasPrefix(strings.ToLower(rule), "not(") && strings.HasSuffix(rule, ")") {
|
|
black = true
|
|
rule = strings.TrimSpace(rule[4 : len(rule)-1])
|
|
}
|
|
matched := false
|
|
for _, item := range strings.Split(rule, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if ipInItem(ip, item) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if black {
|
|
return !matched
|
|
}
|
|
return matched
|
|
}
|
|
|
|
func ipInItem(ip net.IP, item string) bool {
|
|
if strings.Contains(item, "-") {
|
|
parts := strings.SplitN(item, "-", 2)
|
|
lo, hi := net.ParseIP(strings.TrimSpace(parts[0])), net.ParseIP(strings.TrimSpace(parts[1]))
|
|
if lo == nil || hi == nil {
|
|
return false
|
|
}
|
|
v := ip.To4()
|
|
l := lo.To4()
|
|
h := hi.To4()
|
|
if v == nil || l == nil || h == nil {
|
|
return false
|
|
}
|
|
return bytesCompare(v, l) >= 0 && bytesCompare(v, h) <= 0
|
|
}
|
|
return ip.Equal(net.ParseIP(item))
|
|
}
|
|
|
|
func bytesCompare(a, b net.IP) int {
|
|
for i := range a {
|
|
if a[i] < b[i] {
|
|
return -1
|
|
}
|
|
if a[i] > b[i] {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|