From 0087101de9328118f36bc3fd735435c13824a080 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Thu, 9 Dec 2021 20:28:11 -0600 Subject: [PATCH] ok, stepping into an actual structure now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proved that I could manage the protocol with something that was faked, starting to try to serve something for real. The idea is that you would have a directory that contains all of your module zips, and the mir server would serve modules from that directory, and it would query that directory to find out which versions were available and which were latest. I'm not sure about the version list and retractions. It seems like if a module is retracted you still want it to be in the list of available modules, and that a retraction is not signaled by the absence of that version in the list endpoint, but by the module file of the latest version itself. /srv/mir └── modules └── orel.li └── mir@v0.0.0-pre1.zip --- go.mod | 6 +- go.sum | 6 -- handler.go | 254 ++++++++++++++++++++++++++++++++++++++++++----------- serve.go | 21 +++-- 4 files changed, 220 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index 5023d6e..351eb02 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,4 @@ go 1.18 require golang.org/x/mod v0.5.1 -require ( - github.com/jordanorelli/lexnum v0.0.0-20141216151731-460eeb125754 // indirect - github.com/jordanorelli/serve v0.0.0-20190310214448-81022000f440 // indirect - golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect -) +require golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect diff --git a/go.sum b/go.sum index b766615..7214757 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,3 @@ -github.com/jordanorelli/lexnum v0.0.0-20141216151731-460eeb125754 h1:ovgRFhVUYZWz6KnWPrnV7HBxrK0ErOeyXtlVvh0Rr5k= -github.com/jordanorelli/lexnum v0.0.0-20141216151731-460eeb125754/go.mod h1:f1WdQhB98V35bULPsZUMFP9U1XWhpaHrO6myMijgMhU= -github.com/jordanorelli/serve v0.0.0-20190310214448-81022000f440 h1:of3Mn87FnYXxbHQII4Q1IE2en/r903YPKAy7HgMuo68= -github.com/jordanorelli/serve v0.0.0-20190310214448-81022000f440/go.mod h1:AlXw87dXhL0dJjfMQuyQ+7L/rI555OGfF1ctdwVXRzg= -github.com/jordanorelli/tea v0.0.5 h1:fYI1Ag4Ec0Q9KqtjykQQO1dDNJsBFLBhAi04gOSUjOU= -github.com/jordanorelli/tea v0.0.5/go.mod h1:mnnRKfuTTk8d+3rcOC6TIiBOLVG8RQitOtFtCfQo8Bw= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= diff --git a/handler.go b/handler.go index 66469b1..923b140 100644 --- a/handler.go +++ b/handler.go @@ -3,38 +3,46 @@ package main import ( "context" "embed" - "encoding/json" "fmt" "net" "net/http" "os" + "path/filepath" + "regexp" + "strings" "time" - "golang.org/x/mod/module" - "golang.org/x/mod/zip" - - "orel.li/mir/internal/ref" + "golang.org/x/mod/semver" ) //go:embed meta var content embed.FS +// this is pretty janky, but I didn't want to import a routing library +var ( + latestP = regexp.MustCompile(`^/(.+)/@latest$`) + listP = regexp.MustCompile(`^/(.+)/@v/list$`) + infoP = regexp.MustCompile(`^/(.+)/@v/(.+)\.info$`) + modP = regexp.MustCompile(`^/(.+)/@v/(.+)\.mod$`) + zipP = regexp.MustCompile(`^/(.+)/@v/(.+)\.zip$`) +) + type handler struct { - path ref.Ref[string] - index ref.Ref[pathArg] + httpAddr string + socketPath string + root string + hostname string } func (h handler) run() error { - addr, err := net.ResolveUnixAddr("unix", h.path.Val()) - if err != nil { - return fmt.Errorf("bad listen address: %w", err) + if h.hostname == "" { + return fmt.Errorf("hostname missing but hostname is required") } - l, err := net.ListenUnix("unix", addr) + l, err := h.listen() if err != nil { - return fmt.Errorf("unable to open unix socket: %w", err) + return err } - os.Chmod(h.path.Val(), 0777) server := http.Server{ Handler: h, @@ -57,48 +65,192 @@ func (h handler) run() error { return nil } -func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log_info.Printf("%s %s %s", r.Method, r.Host, r.URL.String()) - - switch r.URL.Path { - case "/fart": - // Step 1: a request comes in at orel.li. This page contains a meta tag - // indicating where the package contents may be found, and which backend - // is serving the package. - serveFile(w, "meta/fart/root.html") - case "/modules/orel.li/fart/@v/list": - // Step 2: list all of the versions for the package. Versions may be - // available but unlisted. - serveFile(w, "meta/fart/version-list") - case "/modules/orel.li/fart/@latest", - "/modules/orel.li/fart/@v/v0.0.3.info": - // Step 3: get info for the version, which is just a timestamp at the - // moment. - e := json.NewEncoder(w) - e.Encode(versionInfo{ - Version: "v0.0.3", - Time: time.Now(), - }) - case "/modules/orel.li/fart/@v/v0.0.3.mod": - // Step 4: retrieve the modfile for the package, informing go mod of - // any transitive dependencies. - serveFile(w, "meta/fart/modfile") - case "/modules/orel.li/fart/@v/v0.0.3.zip": - // Step 5: retrieve the source code contents for a package, as a - // specially-formatted zip file. - err := zip.CreateFromDir(w, module.Version{ - Path: "orel.li/fart", - Version: "v0.0.3", - }, "/home/jorelli/mir/modules/orel.li/fart") +func (h handler) listen() (net.Listener, error) { + if h.httpAddr == "" && h.socketPath == "" { + return nil, fmt.Errorf("must supply one of -http or -unix") + } + + if h.httpAddr != "" { + if h.socketPath != "" { + return nil, fmt.Errorf("must supply (only) one of -http or -unix: supplied both") + } + + lis, err := net.Listen("tcp", h.httpAddr) if err != nil { - log_error.Printf("zip error: %v", err) + return nil, fmt.Errorf("failed to start http listener: %w", err) } - case "/": - w.WriteHeader(http.StatusOK) - default: + return lis, nil + } + + lis, err := net.Listen("unix", h.socketPath) + if err != nil { + return nil, fmt.Errorf("failed to start unix socket listener: %w", err) + } + // TODO: what should this permission set be? hrm + // TODO: what about the error here? + os.Chmod(h.socketPath, 0777) + return lis, nil +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log_info.Printf("%s %s %s %s", r.Method, r.Host, r.URL.Host, r.URL.String()) + + // this is very stupid but I didn't want to add a routing library + // dependency for five endpoints + + // if matches := latestP.FindStringSubmatch(r.URL.Path); matches != nil { + // modpath := matches[1] + // h.latest(modpath, w, r) + // return + // } + + if matches := listP.FindStringSubmatch(r.URL.Path); matches != nil { + modpath := matches[1] + h.list(modpath, w, r) + return + } + + // if matches := infoP.FindStringSubmatch(r.URL.Path); matches != nil { + // modpath := matches[1] + // modversion := matches[2] + // h.info(modpath, modversion, w, r) + // return + // } + + // if matches := modP.FindStringSubmatch(r.URL.Path); matches != nil { + // modpath := matches[1] + // modversion := matches[2] + // h.modfile(modpath, modversion, w, r) + // return + // } + + // if matches := zipP.FindStringSubmatch(r.URL.Path); matches != nil { + // modpath := matches[1] + // modversion := matches[2] + // h.zipfile(modpath, modversion, w, r) + // return + // } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("not found")) + return + + // switch r.URL.Path { + // case "/fart": + // // Step 1: a request comes in at orel.li. This page contains a meta tag + // // indicating where the package contents may be found, and which backend + // // is serving the package. + // serveFile(w, "meta/fart/root.html") + // case "/modules/orel.li/fart/@v/list": + // // Step 2: list all of the versions for the package. Versions may be + // // available but unlisted. + // serveFile(w, "meta/fart/version-list") + // case "/modules/orel.li/fart/@latest", + // "/modules/orel.li/fart/@v/v0.0.3.info": + // // Step 3: get info for the version, which is just a timestamp at the + // // moment. + // e := json.NewEncoder(w) + // e.Encode(versionInfo{ + // Version: "v0.0.3", + // Time: time.Now(), + // }) + // case "/modules/orel.li/fart/@v/v0.0.3.mod": + // // Step 4: retrieve the modfile for the package, informing go mod of + // // any transitive dependencies. + // serveFile(w, "meta/fart/modfile") + // case "/modules/orel.li/fart/@v/v0.0.3.zip": + // // Step 5: retrieve the source code contents for a package, as a + // // specially-formatted zip file. + // err := zip.CreateFromDir(w, module.Version{ + // Path: "orel.li/fart", + // Version: "v0.0.3", + // }, "/home/jorelli/mir/modules/orel.li/fart") + // if err != nil { + // log_error.Printf("zip error: %v", err) + // } + // case "/": + // w.WriteHeader(http.StatusOK) + // default: + // w.WriteHeader(http.StatusNotFound) + // w.Write([]byte("not found")) + // } +} + +// locate searches our module root for a given modpath +func (h handler) locate(modpath string) ([]os.DirEntry, error) { + localDir := filepath.Join(h.root, "modules", modpath) + return os.ReadDir(localDir) +} + +func writeError(w http.ResponseWriter, err error) { + if os.IsNotExist(err) { + log_info.Printf("404 %v", err) w.WriteHeader(http.StatusNotFound) - w.Write([]byte("not found")) + fmt.Fprintf(w, "not found") + return } + + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "internal server error") + log_error.Printf("500 %v", err) + return +} + +// latest serves the @latest endpoint +func (h handler) latest(modpath string, w http.ResponseWriter, r *http.Request) { +} + +// list serves the $base/$module/@v/list endpoint +func (h handler) list(modpath string, w http.ResponseWriter, r *http.Request) { + log_info.Printf("list: %s", modpath) + dirpath, _ := filepath.Split(modpath) + log_info.Printf("dirpath: %s", dirpath) + localDir := filepath.Join(h.root, "modules", dirpath) + log_info.Printf("localDir: %s", localDir) + files, err := os.ReadDir(localDir) + if err != nil { + writeError(w, err) + return + } + + allVersions := make([]string, 0, len(files)) + for _, f := range files { + name := f.Name() + if filepath.Ext(name) != ".zip" { + log_info.Printf("not a zip: %s", name) + continue + } + parts := strings.Split(name, "@") + if len(parts) != 2 { + continue + } + if !semver.IsValid(parts[1]) { + continue + } + allVersions = append(allVersions, parts[1]) + } + + semver.Sort(allVersions) + if len(allVersions) == 0 { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, "not found") + return + } + for _, version := range allVersions { + fmt.Fprint(w, version) + } +} + +// info serves the $base/$module/@v/$version.info endpoint +func (h handler) info(modpath, modversion string, w http.ResponseWriter, r *http.Request) { +} + +// modfile serves the $base/$module/@v/$version.mod endpoint +func (h handler) modfile(modpath, modversion string, w http.ResponseWriter, r *http.Request) { +} + +// zipfile serves the $base/$module/@v/$version.zip endpoint +func (h handler) zipfile(modpath, modversion string, w http.ResponseWriter, r *http.Request) { } func serveFile(w http.ResponseWriter, path string) { diff --git a/serve.go b/serve.go index 5e971ec..3c2cfdd 100644 --- a/serve.go +++ b/serve.go @@ -2,21 +2,32 @@ package main import ( "flag" - - "orel.li/mir/internal/ref" ) func serve(args []string) { - path := "./mir.sock" + // listen on this unix domain socket + socketPath := "" + + // serve modules out of this root directory rootDir := "/srv/mir" + // serve module traffic on this hostname + hostname := "" + + httpAddr := "" + serveFlags := flag.NewFlagSet("serve", flag.ExitOnError) - serveFlags.StringVar(&path, "l", path, "path for a unix domain socket to listen on") + serveFlags.StringVar(&socketPath, "unix", socketPath, "path for a unix domain socket to listen on") + serveFlags.StringVar(&httpAddr, "http", httpAddr, "http address to listen on") serveFlags.StringVar(&rootDir, "root", rootDir, "root directory for module storage") + serveFlags.StringVar(&hostname, "hostname", hostname, "domain name on which mir serves modules") serveFlags.Parse(args) h := handler{ - path: ref.New(&path), + socketPath: socketPath, + httpAddr: httpAddr, + root: rootDir, + hostname: hostname, } if err := h.run(); err != nil { bail(1, err.Error())