package fileserver import ( "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "github.com/pquerna/otp/totp" "html/template" "io" "net/http" "os" "path/filepath" "sophuwu.site/cdn/config" "strings" "time" ) type DirEntry struct { Name string FullName string Size int IsDir bool } func (d DirEntry) Si() string { if d.IsDir { return "" } f := float64(d.Size) i := 0 for f > 1024 { f /= 1024 i++ } s := fmt.Sprintf("%.2f", f) s = strings.TrimRight(s, ".0") return fmt.Sprintf("%s %cB", s, " KMGTPEZY"[i]) } type TemplateData struct { Path string Dirs []DirEntry Items []DirEntry } var Temp *template.Template func FillTemp(w io.Writer, path string, items []DirEntry) error { var data = TemplateData{Path: path, Dirs: []DirEntry{}, Items: []DirEntry{}} for _, item := range items { item.FullName = filepath.Join(path, item.Name) if item.IsDir { data.Dirs = append(data.Dirs, item) } else { data.Items = append(data.Items, item) } } return Temp.ExecuteTemplate(w, "index", data) } func FillUpload(w io.Writer, path string) error { return Temp.ExecuteTemplate(w, "index", map[string]string{ "Upload": path, }) } var HttpCodes = map[int]string{ 404: "Not Found", 500: "Internal Server Error", 403: "Forbidden", 401: "Unauthorized", 400: "Bad Request", 200: "OK", } func FillError(w io.Writer, err error, code int) bool { if err == nil { return false } _ = Temp.ExecuteTemplate(w, "index", map[string]string{ "Error": fmt.Sprintf("%d: %s", code, HttpCodes[code]), }) return true } func init() { Temp = template.New("index") Temp.Parse(`{{ define "index" }} {{ if .Path }} {{ .Path }} {{ else }} {{ if .Upload }} Upload {{ else }} Error {{ end }} {{ end }} {{ if .Path }}

Index of: {{ .Path }}

{{ range .Dirs }}
{{ .Name }}{{ .Si }}
{{ end }} {{ range .Items }}
{{ .Name }}{{ .Si }}
{{ end }}
{{ else }} {{ if .Error }}

{{ .Error }}

{{ else }} {{ if .Upload }}

Upload

Path:
File:
Username:
Password:
OTP:
{{ end }} {{ end }} {{ end }} {{ end }}`) } type readseek struct { bb []byte i int } func (r *readseek) Read(p []byte) (n int, err error) { if len(p) > len(r.bb)-r.i { n = len(r.bb) - r.i } else { n = len(p) } for i := 0; i < n; i++ { p[i] = r.bb[r.i+i] } r.i += n if r.i >= len(r.bb) { err = io.EOF } return } func (r *readseek) Seek(offset int64, whence int) (int64, error) { var n int switch whence { case io.SeekStart: n = int(offset) case io.SeekCurrent: n = r.i + int(offset) case io.SeekEnd: n = len(r.bb) + int(offset) } if n < 0 || n > len(r.bb) { return 0, fmt.Errorf("invalid offset") } r.i = n return int64(r.i), nil } func (r *readseek) nullify() { r.bb = []byte{} } func newReadSeek(b []byte) *readseek { return &readseek{b, 0} } type writer struct { bb []byte } func (r *writer) Write(p []byte) (n int, err error) { r.bb = append(r.bb, p...) return len(p), nil } func (r *writer) nullify() { r.bb = []byte{} } func (r *writer) convert() *readseek { return newReadSeek(r.bb) } func newWriter() *writer { return &writer{bb: []byte{}} } func Handler(prefix string, dir func(string) ([]byte, []DirEntry, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ww := newWriter() data, items, err := dir(strings.TrimPrefix(r.URL.Path, prefix)) if !FillError(ww, err, 404) && data != nil { ww.Write(data) } else { err = FillTemp(ww, r.URL.Path, append([]DirEntry{{ Name: "../", FullName: filepath.Dir(strings.TrimSuffix(r.URL.Path, "/")), Size: 0, IsDir: true, }}, items...)) FillError(ww, err, 500) } rs := ww.convert() ww.nullify() w.WriteHeader(http.StatusNotModified) http.ServeContent(w, r, filepath.Base(r.URL.Path), time.Time{}, rs) rs.nullify() }) } func Handle(prefix string, dir func(string) ([]byte, []DirEntry, error)) { http.Handle(prefix, Handler(prefix, dir)) } func VerifyOtp(p, o string) bool { b, err := os.ReadFile(config.OtpPath) if err != nil { return false } s := string(b) s = strings.Split(s, "\n")[0] b, err = base64.StdEncoding.DecodeString(strings.TrimSpace(s)) if err != nil { return false } b = b[:32] bb := sha256.Sum256([]byte(p)) for i := 0; i < len(b); i++ { b[i] = b[i] ^ bb[i] } s = string(b) return totp.Validate(o, s) } func VerifyBasicAuth(u, p string) bool { b, err := os.ReadFile(config.OtpPath) if err != nil { return false } ss := strings.Split(string(b), "\n") if len(ss) < 2 { return false } b, err = base64.StdEncoding.DecodeString(strings.TrimSpace(ss[1])) if err != nil { return false } bb := sha256.Sum256([]byte(u + ";" + p)) return subtle.ConstantTimeCompare(b, bb[:]) == 1 } func Authenticate(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, apass, authOK := r.BasicAuth() if !authOK || !VerifyBasicAuth(user, apass) { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) http.Error(w, "Unauthorized.", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } func UpHandler(prefix string, save func(string, []byte) error) http.Handler { return Authenticate(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { err := FillUpload(w, prefix) if FillError(w, err, 500) { return } return } if r.Method == "POST" { r.ParseMultipartForm(256 * 1024 * 1024) path := r.Form.Get("path") username := r.Form.Get("username") password := r.Form.Get("password") otp := r.Form.Get("otp") if !VerifyOtp(username+";"+password, otp) { FillError(w, fmt.Errorf("unauthorized"), 401) return } file, _, err := r.FormFile("myFile") if FillError(w, err, 400) { return } defer file.Close() data, err := io.ReadAll(file) if FillError(w, err, 500) { return } err = save(path, data) if FillError(w, err, 500) { return } http.Redirect(w, r, strings.ToLower(prefix)+path, http.StatusFound) return } FillError(w, fmt.Errorf("method not allowed"), 405) })) } func UpHandle(prefix string, save func(string, []byte) error) { http.Handle(prefix, UpHandler(prefix, save)) }