diff options
Diffstat (limited to 'src/glaze.go')
-rw-r--r-- | src/glaze.go | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/src/glaze.go b/src/glaze.go new file mode 100644 index 0000000..d0bc3e0 --- /dev/null +++ b/src/glaze.go @@ -0,0 +1,277 @@ +package glaze + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/url" + "os" + "strings" + "syscall" + + + g "gobang" +) + + + +type patternPath struct {} + +type pathInfo struct { + pattern string + path string + label string + fileHandle *os.File + fileInfo fs.FileInfo +} + + + +const ( + xattrTryIndexHtml = "user.glaze.try-index-html" + xattrDirectoryListing = "user.glaze.directory-listing" + xattrExpectedValue = "true" +) + + + +func httpError(w http.ResponseWriter, code int, err error) { + t := http.StatusText(code) + g.Error( + err.Error(), "http-server-error", + "code", code, + "error", err, + "status-text", t, + ) + http.Error(w, t, code) +} + +func withTrailingSlash(path string) string { + if strings.HasSuffix(path, "/") { + return path + } else { + return path + "/" + } +} + +func adjustPattern(pattern string) string { + if pattern == "*" { + return "/" + } else if strings.HasSuffix(pattern, "*") { + return pattern[0:len(pattern) - 1] + } else { + return pattern + "{$}" + } +} + +func handleSymlink(info pathInfo, w http.ResponseWriter, r *http.Request) error { + linked, err := os.Readlink(info.path) + if err != nil { + return err + } + + // From http.Redirect(): + // + // > If the Content-Type header has not been set, + // > Redirect sets it to "text/html; charset=utf-8" and + // > writes a small HTML body. + // + // 🙄 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + http.Redirect(w, r, linked, http.StatusMovedPermanently) + return nil +} + +func handleProxy(info pathInfo, w http.ResponseWriter, r *http.Request) error { + target, err := url.Parse(info.path) + if err != nil { + return err + } + + target.Scheme = "http" + target.Host = "localhost" + httpClient := http.Client { + Transport: &http.Transport { + DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) { + return net.Dial("unix", info.path) + }, + }, + } + + r.URL.Scheme = target.Scheme + r.URL.Host = target.Host + r.RequestURI = "" + response, err := httpClient.Do(r) + if err != nil { + return err + } + for k, vArr := range response.Header { + for _, v := range vArr { + w.Header().Add(k, v) + } + } + w.WriteHeader(response.StatusCode) + io.Copy(w, response.Body) + return nil +} + +func handleFile(info pathInfo, w http.ResponseWriter, r *http.Request) error { + http.ServeContent(w, r, info.fileInfo.Name(), info.fileInfo.ModTime(), info.fileHandle) + return nil +} + +func getXattr(path string, name string) (string, error) { + data := make([]byte, 256) + i, err := syscall.Getxattr(path, name, data) + fmt.Println("i", i) + if err != nil { + return "", err + } + return string(data[0:i]), nil +} + +func handleDirectoryTarget(info pathInfo, w http.ResponseWriter, r *http.Request) error { + targetPath := withTrailingSlash(info.path) + r.URL.Path + newInfo, fn, err := handlerForDynPath(targetPath) + if err != nil { + return err + } + + if (newInfo.label != "directory") { + return fn(newInfo, w, r) + } + + indexHtmlXattr, err := getXattr(targetPath, xattrTryIndexHtml) + if err == nil && string(indexHtmlXattr) == xattrExpectedValue { + newPath := withTrailingSlash(targetPath) + "index.html" + _, err := os.Open(newPath) + if err == nil { + return fn(newInfo, w, r) + } + } + + dirListXattr, err := getXattr(targetPath, xattrDirectoryListing) + if err == nil && string(dirListXattr) == xattrExpectedValue { + http.FileServer(http.Dir(newInfo.path)).ServeHTTP(w, r) + return nil + } + + http.Error(w, "Forbidden", http.StatusForbidden) + return nil +} + +func handlerFuncFor(mode fs.FileMode) (string, func(pathInfo, http.ResponseWriter, *http.Request) error) { + if mode.Type() == fs.ModeSymlink { + return "symlink", handleSymlink + } else if mode.Type() == fs.ModeSocket { + return "socket", handleProxy + } else if !mode.IsDir() { + return "file", handleFile + } else { + return "directory", handleDirectoryTarget + } +} + +func handlerForDynPath(path string) (pathInfo, func(pathInfo, http.ResponseWriter, *http.Request) error, error) { + var info pathInfo + fileHandle, err := os.Open(path) + if err != nil { + return info, nil, err + } + + fileInfo, err := os.Lstat(path) + if err != nil { + return info, nil, err + } + + label, fn := handlerFuncFor(fileInfo.Mode()) + g.Info("Handler picked", "handler-picked", "label", label) + info = pathInfo { + path: path, + label: label, + fileHandle: fileHandle, + fileInfo: fileInfo, + } + return info, fn, nil +} + +func handlerFor(path string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := g.NewUUID() + g.Info("in-request", "in-request", "path", path, "id", id) + defer g.Info("in-response", "in-response", "path", path, "id", id) + + info, fn, err := handlerForDynPath(path) + if err != nil { + httpError(w, http.StatusInternalServerError, err) + return + } + + err = fn(info, w, r) + if err != nil { + httpError(w, http.StatusInternalServerError, err) + return + } + }) +} + +func (i *patternPath) String() string { + return "" +} + +func (_ *patternPath) Set(value string) error { + arr := strings.Split(value, ":") + if len(arr) != 2 { + return errors.New("Bad value for path pattern: " + value) + } + + pattern := adjustPattern(arr[0]) + path := arr[1] + + http.Handle(pattern, http.StripPrefix(pattern, handlerFor(path))) + return nil +} + +func parseArgs(args []string) string { + var pat patternPath + fs := flag.NewFlagSet(args[0], flag.ExitOnError) + fs.Var(&pat, "P", "") + fs.Parse(args[1:]) + if fs.NArg() != 1 { + fmt.Fprintf( + os.Stderr, + "Usage: %s [ -P PATTERN:PATH ]... LISTEN.socket\n", + args[0], + ) + os.Exit(2) + } + return fs.Arg(0) +} + +func listen(fromAddr string) net.Listener { + listener, err := net.Listen("unix", fromAddr) + g.FatalIf(err) + g.Info("Started listening", "listen-start", "from-address", fromAddr) + return listener +} + +func start(listener net.Listener) { + server := http.Server {} + err := server.Serve(listener) + g.FatalIf(err) +} + + + +func Main() { + g.Init() + addr := parseArgs(os.Args) + listener := listen(addr) + start(listener) +} |