Files
CHFS-NG/chfs-ng/internal/api/server.go
T

589 lines
15 KiB
Go

package api
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"path"
"path/filepath"
"strings"
"time"
"chfs-ng/internal/auth"
"chfs-ng/internal/storage"
"github.com/emersion/go-webdav"
)
type Server struct {
Store storage.Storage
Auth *auth.Manager
Sessions map[string]*auth.Account
WebDAVRoot string
Allow string
}
type fileInfo 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"`
}
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/preview", s.preview)
mux.HandleFunc("/", s.fileHandler)
return s.ipFilter(mux)
}
func account(r *http.Request, s *Server) *auth.Account {
if c, e := r.Cookie("chfs_token"); e == nil {
if a := s.Sessions[c.Value]; a != nil {
return a
}
}
return s.Auth.Guest
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(v)
}
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 "d"
}
if p.HasWrite() {
return "w"
}
if p.HasRead() {
return "r"
}
return ""
}
func (s *Server) files(w http.ResponseWriter, r *http.Request) {
a := account(r, s)
p := qpath(r)
m := a.MaskFor(p)
if !m.HasRead() {
http.Error(w, "forbidden", 403)
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())
sub := false
if fi.IsDir() {
if xs, e := s.Store.List(fp); e == nil {
sub = len(xs) > 0
}
}
out = append(out, fileInfo{Name: fi.Name(), Path: fp, Size: fi.Size(), Modified: fi.ModTime().Format(time.RFC3339), Dir: fi.IsDir(), Mask: permString(a.MaskFor(fp)), HasSubDir: sub})
}
writeJSON(w, map[string]any{"files": out, "currentMask": permString(m), "virtualRoot": false})
}
func (s *Server) newdir(w http.ResponseWriter, r *http.Request) {
_ = r.ParseMultipartForm(16 << 20)
_ = r.ParseForm()
a := account(r, s)
p := qpath(r)
if !a.MaskFor(p).HasWrite() {
http.Error(w, "forbidden", 403)
return
}
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
}
if e := s.Store.MkdirAll(p); e != nil {
http.Error(w, e.Error(), 500)
return
}
writeJSON(w, map[string]any{"ok": true})
}
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 !a.MaskFor(old).HasWrite() {
http.Error(w, "forbidden", 403)
return
}
if e := s.Store.Rename(old, neu); e != nil {
http.Error(w, e.Error(), 500)
return
}
writeJSON(w, map[string]any{"ok": true})
}
func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
a := account(r, s)
_ = r.ParseForm()
items := r.Form["filepath"]
if len(items) == 0 {
items = r.Form["path"]
}
for _, p := range items {
if !a.MaskFor(p).HasDelete() {
http.Error(w, "forbidden", 403)
return
}
if e := s.Store.Remove(p); e != nil {
http.Error(w, e.Error(), 500)
return
}
}
writeJSON(w, map[string]any{"ok": true})
}
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)
writeJSON(w, map[string]any{"exist": e == nil})
}
func (s *Server) session(w http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" {
http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
writeJSON(w, map[string]any{"ok": true})
return
}
if r.Method == "GET" {
user := ""
if c, e := r.Cookie("chfs_token"); e == nil {
if x := s.Sessions[c.Value]; x != nil {
user = x.Username
}
}
writeJSON(w, map[string]any{"login": user != "", "user": user})
return
}
user, pwd := r.FormValue("user"), r.FormValue("pwd")
a := s.Auth.Authenticate(user, pwd)
if a == nil {
http.Error(w, "invalid account", 401)
return
}
token := auth.NewToken()
s.Sessions[token] = a
http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode})
writeJSON(w, map[string]any{"ok": true, "user": a.Username})
}
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() {
http.Error(w, "forbidden", 403)
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"))
writeJSON(w, map[string]any{"ok": true})
}
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() {
http.Error(w, "forbidden", 403)
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
}
writeJSON(w, map[string]any{"ok": true})
}
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() {
http.Error(w, "forbidden", 403)
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)
}
}
w.WriteHeader(http.StatusCreated)
writeJSON(w, map[string]any{"ok": true, "files": saved})
}
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() {
http.Error(w, "forbidden", 403)
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))
}
}
w.WriteHeader(http.StatusCreated)
writeJSON(w, map[string]any{"ok": true, "files": saved})
}
func (s *Server) search(w http.ResponseWriter, r *http.Request) {
a := account(r, s)
if !a.MaskFor("").HasRead() {
http.Error(w, "forbidden", 403)
return
}
needle := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("str")))
if needle == "" {
writeJSON(w, map[string]any{"files": []fileInfo{}})
return
}
var out []fileInfo
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 strings.Contains(strings.ToLower(fi.Name()), needle) {
out = append(out, fileInfo{Name: fi.Name(), Path: p, Size: fi.Size(), Modified: fi.ModTime().Format(time.RFC3339), Dir: fi.IsDir(), Mask: permString(a.MaskFor(p)), GuestMask: permString(s.Auth.Guest.MaskFor(p))})
}
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{"files": out})
}
func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
file := strings.Trim(r.URL.Query().Get("file"), "/")
if file == "" {
file = strings.TrimPrefix(r.URL.Path, "/chfs/shared/")
}
s.serveFile(w, r, file, true)
}
func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
if !account(r, s).MaskFor(strings.Trim(r.URL.Query().Get("file"), "/")).HasRead() {
http.Error(w, "forbidden", 403)
return
}
file := strings.Trim(r.URL.Query().Get("file"), "/")
if file == "" {
http.Error(w, "file required", 400)
return
}
s.serveFile(w, r, file, false)
}
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() {
http.Error(w, "forbidden", 403)
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()))
}
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() {
http.Error(w, "forbidden", 403)
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
}
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.TrimPrefix(r.URL.Path, "/webdav")
vp = strings.Trim(vp, "/")
if !account(r, s).MaskFor(vp).HasRead() {
http.Error(w, "forbidden", 403)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead && !account(r, s).MaskFor(vp).HasWrite() {
http.Error(w, "forbidden", 403)
return
}
old := r.URL.Path
r.URL.Path = "/" + vp
defer func() { r.URL.Path = old }()
h := &webdav.Handler{FileSystem: webdav.LocalFileSystem(s.WebDAVRoot)}
h.ServeHTTP(w, r)
}
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
}