package main import ( "errors" "fmt" "io" "io/fs" "log" "net/http" "os" "os/user" "path/filepath" "strconv" "strings" "syscall" "git.milar.in/milarin/envvars/v2" "git.milar.in/milarin/slices" ) var ( Directory = envvars.String("ROOT_DIRECTORY", ".") Interface = envvars.String("HTTP_INTERFACE", "") Port = envvars.Uint16("HTTP_PORT", 80) NoAuth = envvars.Bool("NO_AUTH", false) AllowedMethods = envvars.StringSlice("HTTP_METHODS", ",", []string{"GET", "PUT", "HEAD", "DELETE", "COPY", "LINK"}) ) func main() { fmt.Println("Root Directory:", Directory) fmt.Println("HTTP interface:", Interface) fmt.Println("HTTP port:", Port) fmt.Println("Basic Auth enabled:", !NoAuth) fmt.Println("Allowed HTTP methods:", strings.Join(AllowedMethods, ",")) http.HandleFunc("/", handler) err := http.ListenAndServe(fmt.Sprintf("%s:%d", Interface, Port), nil) if err != nil { panic(err) } } func handler(w http.ResponseWriter, r *http.Request) { log.Println(r.Method, getPath(r.URL.Path, r)) if !slices.Contains(AllowedMethods, r.Method) { w.Header().Add("Allow", strings.Join(AllowedMethods, ", ")) http.Error(w, "invalid method", http.StatusMethodNotAllowed) return } switch r.Method { case "GET": get(w, r) case "PUT": put(w, r) // TODO accept Range header for PUT requests case "HEAD": head(w, r) case "DELETE": delete(w, r) case "COPY": copy(w, r) case "LINK": link(w, r) } } func get(w http.ResponseWriter, r *http.Request) { path := getPath(r.URL.Path, r) http.ServeFile(w, r, path) } func put(w http.ResponseWriter, r *http.Request) { path := getPath(r.URL.Path, r) if r.Header.Get("Content-Type") == "inode/directory" { err := os.MkdirAll(path, os.ModePerm) if err != nil { handleError(w, r, err) } return } err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { handleError(w, r, err) return } file, err := os.Create(path) if err != nil { handleError(w, r, err) return } defer file.Close() io.Copy(file, r.Body) } func head(w http.ResponseWriter, r *http.Request) { path := getPath(r.URL.Path, r) fi, err := os.Stat(path) if err != nil { handleError(w, r, err) return } w.Header().Add("Content-Type", getMimetype(path)) w.Header().Add("File-Permissions", fi.Mode().Perm().String()) w.Header().Add("Modified-Time", fi.ModTime().String()) username, groupname, err := getOwnerNames(getOwnerIDs(fi)) if err == nil { w.Header().Add("File-Owner", username) w.Header().Add("Group-Owner", groupname) } } func delete(w http.ResponseWriter, r *http.Request) { path := getPath(r.URL.Path, r) err := os.RemoveAll(path) if err != nil { handleError(w, r, err) return } } func copy(w http.ResponseWriter, r *http.Request) { destinations, ok := r.Header["Destination"] if !ok { w.WriteHeader(http.StatusBadRequest) fmt.Fprintln(w, "missing Destination header") } current_path := getPath(r.URL.Path, r) destination_paths := make([]string, 0, len(destinations)) for _, destination := range destinations { destination_paths = append(destination_paths, getPath(destination, r)) } current_file, err := os.Open(current_path) if err != nil { handleError(w, r, err) return } defer current_file.Close() destination_files := make([]io.Writer, 0, len(destination_paths)) for _, destination_path := range destination_paths { destination_file, err := os.Create(destination_path) if err != nil { handleError(w, r, err) return } defer destination_file.Close() destination_files = append(destination_files, destination_file) } io.Copy(io.MultiWriter(destination_files...), current_file) } func link(w http.ResponseWriter, r *http.Request) { _, ok := r.Header["Link"] if !ok { w.WriteHeader(http.StatusBadRequest) fmt.Fprintln(w, "missing Destination header") } current_path := getPath(r.URL.Path, r) destination_path := getPath(r.Header.Get("Link"), r) err := os.Symlink(current_path, destination_path) if err != nil { handleError(w, r, err) return } } func handleError(w http.ResponseWriter, r *http.Request, err error) { if errors.Is(err, os.ErrNotExist) { w.WriteHeader(http.StatusNotFound) } else if errors.Is(err, errInvalidRange) { w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) } else { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, err) } } func getOwnerIDs(fi fs.FileInfo) (uid, gid string) { if stat, ok := fi.Sys().(*syscall.Stat_t); ok { return strconv.Itoa(int(stat.Uid)), strconv.Itoa(int(stat.Gid)) } return "", "" } func getOwnerNames(uid, gid string) (username, groupname string, err error) { usr, err := user.LookupId(uid) if err != nil { return "", "", err } grp, err := user.LookupGroupId(gid) if err != nil { return "", "", err } return usr.Username, grp.Name, nil } func getPath(dirty_path string, r *http.Request) string { if NoAuth { return filepath.Join(Directory, filepath.Clean(dirty_path)) } username, _, _ := r.BasicAuth() return filepath.Join(Directory, filepath.Clean(username), filepath.Clean(dirty_path)) }