package main import ( "encoding/base64" "errors" "flag" "fmt" "io" "io/fs" "log" "net/http" "net/url" "os" "os/user" "path/filepath" "strconv" "strings" "syscall" "git.tordarus.net/Tordarus/dockerhealth" ) var ( directory = flag.String("d", ".", "directory") intf = flag.String("i", "", "interface") port = flag.Uint("p", 80, "port") noauth = flag.Bool("a", false, "dont use basic auth") ) var ( fileServer http.Handler ) func main() { flag.Parse() fileServer = http.FileServer(http.Dir(*directory)) http.HandleFunc("/", handler) dockerhealth.Healthy = true err := http.ListenAndServe(fmt.Sprintf("%s:%d", *intf, *port), nil) if err != nil { panic(err) } } func handler(w http.ResponseWriter, r *http.Request) { log.Println(r.Method, getPath(r.URL.Path, r)) 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) 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) } if fi.IsDir() { files, err := os.ReadDir(path) if err != nil { handleError(w, r, err) return } for _, file := range files { fmt.Fprintln(w, url.QueryEscape(file.Name())) } return } file, err := os.Open(path) if err != nil { handleError(w, r, err) return } defer file.Close() io.Copy(w, file) } 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) } fileServer.ServeHTTP(w, r) } 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, _, _ := basicAuth(r.Header.Get("Authorization")) return filepath.Join(*directory, filepath.Clean(username), filepath.Clean(dirty_path)) } func basicAuth(authHeader string) (username, password string, err error) { data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) if err != nil { return "", "", err } splits := strings.SplitN(string(data), ":", 2) if len(splits) < 2 { return "", "", errors.New("invalid auth header") } return splits[0], splits[1], nil }