ok, stepping into an actual structure now

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
master
Jordan Orelli 3 years ago
parent 042ccd2d73
commit 0087101de9

@ -4,8 +4,4 @@ go 1.18
require golang.org/x/mod v0.5.1 require golang.org/x/mod v0.5.1
require ( require golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
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
)

@ -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 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=

@ -3,38 +3,46 @@ package main
import ( import (
"context" "context"
"embed" "embed"
"encoding/json"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"regexp"
"strings"
"time" "time"
"golang.org/x/mod/module" "golang.org/x/mod/semver"
"golang.org/x/mod/zip"
"orel.li/mir/internal/ref"
) )
//go:embed meta //go:embed meta
var content embed.FS 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 { type handler struct {
path ref.Ref[string] httpAddr string
index ref.Ref[pathArg] socketPath string
root string
hostname string
} }
func (h handler) run() error { func (h handler) run() error {
addr, err := net.ResolveUnixAddr("unix", h.path.Val()) if h.hostname == "" {
if err != nil { return fmt.Errorf("hostname missing but hostname is required")
return fmt.Errorf("bad listen address: %w", err)
} }
l, err := net.ListenUnix("unix", addr) l, err := h.listen()
if err != nil { if err != nil {
return fmt.Errorf("unable to open unix socket: %w", err) return err
} }
os.Chmod(h.path.Val(), 0777)
server := http.Server{ server := http.Server{
Handler: h, Handler: h,
@ -57,48 +65,192 @@ func (h handler) run() error {
return nil return nil
} }
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h handler) listen() (net.Listener, error) {
log_info.Printf("%s %s %s", r.Method, r.Host, r.URL.String()) if h.httpAddr == "" && h.socketPath == "" {
return nil, fmt.Errorf("must supply one of -http or -unix")
switch r.URL.Path { }
case "/fart":
// Step 1: a request comes in at orel.li. This page contains a meta tag if h.httpAddr != "" {
// indicating where the package contents may be found, and which backend if h.socketPath != "" {
// is serving the package. return nil, fmt.Errorf("must supply (only) one of -http or -unix: supplied both")
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 lis, err := net.Listen("tcp", h.httpAddr)
// available but unlisted. if err != nil {
serveFile(w, "meta/fart/version-list") return nil, fmt.Errorf("failed to start http listener: %w", err)
case "/modules/orel.li/fart/@latest", }
"/modules/orel.li/fart/@v/v0.0.3.info": return lis, nil
// Step 3: get info for the version, which is just a timestamp at the }
// moment.
e := json.NewEncoder(w) lis, err := net.Listen("unix", h.socketPath)
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 { if err != nil {
log_error.Printf("zip error: %v", err) return nil, fmt.Errorf("failed to start unix socket listener: %w", err)
} }
case "/": // TODO: what should this permission set be? hrm
w.WriteHeader(http.StatusOK) // TODO: what about the error here?
default: 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.WriteHeader(http.StatusNotFound)
w.Write([]byte("not found")) 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)
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) { func serveFile(w http.ResponseWriter, path string) {

@ -2,21 +2,32 @@ package main
import ( import (
"flag" "flag"
"orel.li/mir/internal/ref"
) )
func serve(args []string) { func serve(args []string) {
path := "./mir.sock" // listen on this unix domain socket
socketPath := ""
// serve modules out of this root directory
rootDir := "/srv/mir" rootDir := "/srv/mir"
// serve module traffic on this hostname
hostname := ""
httpAddr := ""
serveFlags := flag.NewFlagSet("serve", flag.ExitOnError) 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(&rootDir, "root", rootDir, "root directory for module storage")
serveFlags.StringVar(&hostname, "hostname", hostname, "domain name on which mir serves modules")
serveFlags.Parse(args) serveFlags.Parse(args)
h := handler{ h := handler{
path: ref.New(&path), socketPath: socketPath,
httpAddr: httpAddr,
root: rootDir,
hostname: hostname,
} }
if err := h.run(); err != nil { if err := h.run(); err != nil {
bail(1, err.Error()) bail(1, err.Error())

Loading…
Cancel
Save