From: www Date: Sun, 29 Sep 2024 21:30:07 +0000 (+0000) Subject: Mirrored from toyohime.git X-Git-Url: https://git.chaotic.ninja/gitweb/yakumo_izuru/?a=commitdiff_plain;h=31d6b665fa8c3adc54313973c7e21bc3d4a9f6e3;p=toyohime.git Mirrored from toyohime.git git-svn-id: https://svn.chaotic.ninja/svn/toyohime-yakumo.izuru@1 54e66ccc-7408-294c-82d8-82ecf80158e7 --- 31d6b665fa8c3adc54313973c7e21bc3d4a9f6e3 diff --git a/branches/master/.gitignore b/branches/master/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/branches/master/LICENSE b/branches/master/LICENSE new file mode 100644 index 0000000..d101e81 --- /dev/null +++ b/branches/master/LICENSE @@ -0,0 +1,42 @@ +Copyright (c) 2018, Jon Betti +Copyright (c) 2023, Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +Copyright (c) 2016, Kare Nuorteva +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/branches/master/Makefile b/branches/master/Makefile new file mode 100644 index 0000000..903799b --- /dev/null +++ b/branches/master/Makefile @@ -0,0 +1,26 @@ +GO ?= go +GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=$(VERSION) -X `go list`.Commit=$(COMMIT) -X `go list`.Build=$(BUILD)" -tags "static_build" +PREFIX ?= /usr/local + +VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"` +COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"` +BRANCH = `git rev-parse --abbrev-ref HEAD` +BUILD = `git show -s --pretty=format:%cI` + +GOARCH ?= amd64 +GOOS ?= linux + +all: toyohime + +toyohime: + ${GO} build ${GOFLAGS} ./cmd/toyohime +clean: + rm toyohime +install: + install -m0755 toyohime ${DESTDIR}${PREFIX}/bin/yorihime + install -Dm0044 toyohime.1 ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 +test: + ${GO} test . +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/toyohime + rm -f ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 diff --git a/branches/master/README.md b/branches/master/README.md new file mode 100644 index 0000000..08a2111 --- /dev/null +++ b/branches/master/README.md @@ -0,0 +1,25 @@ +# Toyohime +Fork of [go.jonnrb.io/vanity](https://go.jonnrb.io/vanity) + +A vanity import path is any import path that can be downloaded with +`go get` but isn't otherwise blessed by the `go` tool (e.g. GitHub, +BitBucket, etc.). A commonly used vanity import path is +"golang.org/x/...". This package attempts to mimic the behavior of +"golang.org/x/..." as closely as possible. + +## Features + +* Redirects browsers to godocs.io (or somewhere else) +* Redirects Go tool to VCS +* Redirects pkg.go.dev to browsable files + +## Installation + +```bash +go install marisa.chaotic.ninja/toyohime/cmd/toyohime@latest +``` + +## Specification + - [Remote Import Paths](https://golang.org/cmd/go/#hdr-Remote_import_paths) + - [GDDO Source Code Links](https://github.com/golang/gddo/wiki/Source-Code-Links) + - [Custom Import Path Checking](https://docs.google.com/document/d/1jVFkZTcYbNLaTxXD9OcGfn7vYv5hWtPx9--lTx1gPMs/edit) diff --git a/branches/master/cmd/toyohime/README.md b/branches/master/cmd/toyohime/README.md new file mode 100644 index 0000000..0bf7f81 --- /dev/null +++ b/branches/master/cmd/toyohime/README.md @@ -0,0 +1,21 @@ +# Toyohime (command) + +Runs a barebones vanity server over HTTP. + +## Usage + +``` +./toyohime [-index] fqdn [repo file] +``` + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +If repo file is not given, "./repos" is used. The file has the following format: + +``` +pkgroot vcsScheme://vcsHost/user/repo +pkgroot2 vcsScheme://vcsHost/user/repo2 +``` + +vcsHost is either a [Gogs](https://gogs.io) server (that's what I use) or [GitHub](https://github.com). I'm open to supporting other VCSs but I'm not sure what that would look like. diff --git a/branches/master/cmd/toyohime/dynamic_handler.go b/branches/master/cmd/toyohime/dynamic_handler.go new file mode 100644 index 0000000..9087230 --- /dev/null +++ b/branches/master/cmd/toyohime/dynamic_handler.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + "net/http" + "sync" + + "github.com/fsnotify/fsnotify" +) + +type dynamicHandler struct { + *fsnotify.Watcher + + h http.Handler + unhealthy bool + mu sync.RWMutex +} + +func newDynamicHandler(file string, generator func() (http.Handler, error)) *dynamicHandler { + w, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("Failed to create fsnotify.Watcher: %v", err) + } + + if err := w.Add(file); err != nil { + log.Fatalf("Could not watch file %q: %v", file, err) + } + + h, err := generator() + if err != nil { + log.Fatalf("Failed generating initial handler: %v", err) + } + + dh := &dynamicHandler{Watcher: w, h: h} + updateHandler := func(h http.Handler, err error) error { + dh.mu.Lock() + defer dh.mu.Unlock() + + if err != nil { + dh.unhealthy = true + return err + } + dh.unhealthy = false + + if h == nil { + panic("nil handler returned from generator") + } + dh.h = h + return nil + } + + go func() { + log.Printf("Watching for changes to %q", file) + for { + select { + case evt, ok := <-w.Events: + if !ok { + return + } + if evt.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename) == 0 { + continue + } + if err := updateHandler(generator()); err != nil { + log.Printf("Error switching to new handler: %v", err) + } else { + log.Printf("Updated to new handler based on %q", file) + } + case err, ok := <-w.Errors: + if !ok { + return + } + log.Printf("Error in fsnotify.Watcher: %v", err) + } + } + }() + + return dh +} + +func (dh *dynamicHandler) IsHealthy() bool { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return !dh.unhealthy +} + +func (dh *dynamicHandler) getHandler() http.Handler { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return dh.h +} + +func (dh *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dh.getHandler().ServeHTTP(w, r) +} diff --git a/branches/master/cmd/toyohime/main.go b/branches/master/cmd/toyohime/main.go new file mode 100644 index 0000000..2204d40 --- /dev/null +++ b/branches/master/cmd/toyohime/main.go @@ -0,0 +1,282 @@ +/* +Runs a barebones vanity server over HTTP. + +Usage + + ./toyohime [-index] [-nohealthz] fqdn [repo file] + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +The "-nohealthz" flag disables the "/healthz" endpoint that returns a 200 OK +when everything is OK. + +The "-watch" flag watches the repo file for changes. When it is updated, the +updated version will be used for serving. + +If repo file is not given, "./repos" is used. The file has the following format: + + pkgroot vcsScheme://vcsHost/user/repo + pkgroot2 vcsScheme://vcsHost/user/repo2 + +vcsHost is either a Gogs server (that's what I use) or github.com. I'm open to +supporting other VCSs but I'm not sure what that would look like. +*/ +package main // go.jonnrb.io/vanity + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "html/template" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "marisa.chaotic.ninja/toyohime" +) + +var ( + showIndex = flag.Bool("index", false, "Show a list of repos at /") + noHealthz = flag.Bool("nohealthz", false, "Disable healthcheck endpoint at /healthz") + watch = flag.Bool("watch", false, "Watch repos file for changes and reload") + listenPort = flag.String("listen", ":8080", "Port to bind on") +) + +var ( + host string // param 1 + reposPath string = "repos" // param 2 +) + +func serveRepo(mux *http.ServeMux, root string, u *url.URL) { + vcsScheme, vcsHost := u.Scheme, u.Host + + // Get ["", "user", "repo"]. + pathParts := strings.Split(u.Path, "/") + if len(pathParts) != 3 { + log.Fatalf("Repo URL must be of the form vcsScheme://vcsHost/user/repo but got %q", u.String()) + } + user, repo := pathParts[1], pathParts[2] + + importPath := host + "/" + root + var h http.Handler + if vcsHost == "github.com" { + h = toyohime.GitHubHandler(importPath, user, repo, vcsScheme) + } else { + h = toyohime.GogsHandler(importPath, vcsHost, user, repo, vcsScheme) + } + mux.Handle("/"+root, h) + mux.Handle("/"+root+"/", h) +} + +func addRepoHandlers(mux *http.ServeMux, r io.Reader) error { + indexMap := map[string]string{} + + sc := bufio.NewScanner(r) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + switch len(fields) { + case 0: + continue + case 2: + // Pass + default: + return fmt.Errorf("expected line of form \"path vcsScheme://vcsHost/user/repo\" but got %q", sc.Text()) + } + + if *showIndex { + indexMap[fields[0]] = fields[1] + } + + path := fields[0] + u, err := url.Parse(fields[1]) + if err != nil { + return fmt.Errorf("repo was not a valid URL: %q", fields[1]) + } + + serveRepo(mux, path, u) + } + + if !*showIndex { + return nil + } + + var b bytes.Buffer + err := template.Must(template.New("").Parse(` + + + +Import paths hosted at {{ .Host }} + + + +{{ $host := .Host }} +
+

でホストされているインポート パス {{ html $host }}

+
+
+ +{{ range $root, $repo := .IndexMap }} + +
+
+* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +
+
+
+

{{ html $root }}

+Package
+Repository +{{ else }} +Nothing here. +{{ end }} +
+
+

~から明らかに盗まれた azukifont.com

+
+ + +`)).Execute(&b, struct { + IndexMap map[string]string + Host string + }{ + IndexMap: indexMap, + Host: host, + }) + if err != nil { + return fmt.Errorf("couldn't create index page: %v", err) + } + buf := b.Bytes() + + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + io.Copy(w, bytes.NewReader(buf)) + })) + return nil +} + +func registerHealthz(mux *http.ServeMux, isHealthy func() bool) { + mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isHealthy() { + io.WriteString(w, "OK\r\n") + } else { + http.Error(w, "internal error\r\n", http.StatusInternalServerError) + } + })) +} + +var healthcheck = func() bool { + return true +} + +func generateHandler() (http.Handler, error) { + mux := http.NewServeMux() + + f, err := os.Open(reposPath) + if err != nil { + return nil, fmt.Errorf("error opening %q: %v", reposPath, err) + } + if err := addRepoHandlers(mux, f); err != nil { + return nil, err + } + + if !*noHealthz { + registerHealthz(mux, healthcheck) + } + return mux, nil +} + +func buildServer(h http.Handler) *http.Server { + return &http.Server{ + // This should be sufficient. + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 5 * time.Second, + + Addr: *listenPort, + Handler: h, + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: %s fqdn [repos file]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + host = flag.Arg(0) + if host == "" { + flag.Usage() + os.Exit(-1) + } + + if override := flag.Arg(1); override != "" { + reposPath = override + } + + var h http.Handler + if *watch { + dh := newDynamicHandler(reposPath, generateHandler) + healthcheck = dh.IsHealthy + defer dh.Close() + h = dh + } else { + var err error + h, err = generateHandler() + if err != nil { + log.Printf("Error generating handler: %v", err) + } + } + + srv := buildServer(h) + + log.Printf("starting toyohime %v on port %v\n", toyohime.FullVersion(), *listenPort) + log.Println(srv.ListenAndServe()) +} diff --git a/branches/master/go.mod b/branches/master/go.mod new file mode 100644 index 0000000..e2712e7 --- /dev/null +++ b/branches/master/go.mod @@ -0,0 +1,5 @@ +module marisa.chaotic.ninja/toyohime + +go 1.14 + +require github.com/fsnotify/fsnotify v1.4.9 diff --git a/branches/master/go.sum b/branches/master/go.sum new file mode 100644 index 0000000..b12c1ed --- /dev/null +++ b/branches/master/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/branches/master/rc.d/freebsd.sh b/branches/master/rc.d/freebsd.sh new file mode 100644 index 0000000..005a6e6 --- /dev/null +++ b/branches/master/rc.d/freebsd.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON NETWORKING +# BEFORE: LOGIN +# KEYWORD: shutdown + +. /etc/rc.subr + +name="toyohime" +rcvar="${name}_enable" + +: ${toyohime_enable="NO"} +: ${toyohime_fqdn="localhost"} +: ${toyohime_user="www"} +: ${toyohime_group="www"} +: ${toyohime_repos="/usr/local/etc/toyohime-repos"} +: ${toyohime_address="127.0.0.1:8080"} + +command="/usr/sbin/daemon" +pidfile="/var/run/${name}.pid" +command_args="-p ${pidfile} -u ${toyohime_user} /usr/local/bin/${name} -listen ${toyohime_address} -index -watch ${toyohime_fqdn} ${toyohime_repos}" + +load_rc_config "${name}" +run_rc_command "$1" diff --git a/branches/master/rc.d/immortal.yml b/branches/master/rc.d/immortal.yml new file mode 100644 index 0000000..7e0008b --- /dev/null +++ b/branches/master/rc.d/immortal.yml @@ -0,0 +1,2 @@ +cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos +user: www diff --git a/branches/master/rc.d/netbsd.sh b/branches/master/rc.d/netbsd.sh new file mode 100644 index 0000000..8c9cdd6 --- /dev/null +++ b/branches/master/rc.d/netbsd.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON +# BEFORE: LOGIN +# KEYWORD: shutdown + +$rc_subr_loaded . /etc/rc.subr + +name="toyohime" +rcvar="$name" +command="/usr/local/bin/$name" +command_args="-listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos" + +load_rc_config "$name" +run_rc_command "$1" diff --git a/branches/master/rc.d/openbsd.ksh b/branches/master/rc.d/openbsd.ksh new file mode 100644 index 0000000..5c56be0 --- /dev/null +++ b/branches/master/rc.d/openbsd.ksh @@ -0,0 +1,11 @@ +#!/bin/ksh +# $TheSupernovaDuo$ + +. /etc/rc.d/rc.subr + +daemon="/usr/local/bin/toyohime" +daemon_flags="-listen 127.0.0.1:8080 -index localhost /etc/toyohime-repos" +daemon_user="www" +rc_bg=YES + +rc_cmd "$1" diff --git a/branches/master/toyohime.1 b/branches/master/toyohime.1 new file mode 100644 index 0000000..7ba383f --- /dev/null +++ b/branches/master/toyohime.1 @@ -0,0 +1,25 @@ +.Dd $Mdocdate$ +.Dt TOYOHIME 1 +.Os +.Sh NAME +.Nm toyohime +.Nd Library and CLI for hosting custom vanity URIs for the Go tool +.Sh SYNOPSIS +.Nm +.Op Fl index +.Op Fl listen Ar ip:port +.Op Fl nohealthz +.Op Fl watch +.Op Ar fqdn +.Op Ar path/to/repos/file +.Sh DESCRIPTION +.Nm +is a library and command line implementation +that allows developers to have their own path +for their Go packages, closely replicating +the behavior of golang.org/x/.. as much as +possible. +.Sh AUTHORS +.An Jon Betti Aq Mt jonbetti@gmail.com +.Sh MAINTAINERS +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/branches/master/toyohime.go b/branches/master/toyohime.go new file mode 100644 index 0000000..0439d4d --- /dev/null +++ b/branches/master/toyohime.go @@ -0,0 +1,179 @@ +/* +Package toyohime implements custom import paths (Go vanity URLs) as an HTTP +handler that can be installed at the vanity URL. +*/ +package toyohime // import "go.jonnrb.io/vanity" <- original one for reference + +import ( + "fmt" + "html/template" + "net/http" + "strings" +) + +type config struct { + importTag *string + sourceTag *string + redir Redirector +} + +// Configures the Handler. The only required option is WithImport. +type Option func(*config) + +// Instructs the go tool where to fetch the repo at vcsRoot and the importPath +// that tree should be rooted at. +func WithImport(importPath, vcs, vcsRoot string) Option { + importTag := "" + return func(cfg *config) { + if cfg.importTag != nil { + panic(fmt.Sprintf("vanity: existing import tag: %s", *cfg.importTag)) + } + cfg.importTag = &importTag + } +} + +// Instructs gddo (godoc.org) how to direct browsers to browsable source code +// for packages and their contents rooted at prefix. +// +// home specifies the home page of prefix, directory gives a format for how to +// browse a directory, and file gives a format for how to view a file and go to +// specific lines within it. +// +// More information can be found at https://github.com/golang/gddo/wiki/Source-Code-Links. +// +func WithSource(prefix, home, directory, file string) Option { + sourceTag := "" + return func(cfg *config) { + if cfg.sourceTag != nil { + panic(fmt.Sprintf("vanity: existing source tag: %s", *cfg.importTag)) + } + cfg.sourceTag = &sourceTag + } +} + +// When a browser navigates to the vanity URL of pkg, this function rewrites +// pkg to a browsable URL. +type Redirector func(pkg string) (url string) + +func WithRedirector(redir Redirector) Option { + return func(cfg *config) { + if cfg.redir != nil { + panic("vanity: existing Redirector") + } + cfg.redir = redir + } +} + +func compile(opts []Option) (*template.Template, Redirector) { + // Process options. + var cfg config + for _, opt := range opts { + opt(&cfg) + } + + // A WithImport is required. + if cfg.importTag == nil { + panic("vanity: WithImport is required") + } + + tags := []string{*cfg.importTag} + if cfg.sourceTag != nil { + tags = append(tags, *cfg.sourceTag) + } + tagBlk := strings.Join(tags, "\n") + + h := fmt.Sprintf(` + + + +%s + + + +Nothing to see here; move along. + + +`, tagBlk) + + // Use default GDDO Redirector. + if cfg.redir == nil { + cfg.redir = func(pkg string) string { + return "https://pkg.go.dev/" + pkg + } + } + + return template.Must(template.New("").Parse(h)), cfg.redir +} + +func handlerFrom(tpl *template.Template, redir Redirector) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only method supported is GET. + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(w, http.StatusText(status), status) + return + } + + pkg := r.Host + r.URL.Path + redirURL := redir(pkg) + + // Issue an HTTP redirect if this is definitely a browser. + if r.FormValue("go-get") != "1" { + http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + if err := tpl.ExecuteTemplate(w, "", redirURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + +// Returns an http.Handler that serves the vanity URL information for a single +// repository. Each Option gives additional information to agents about the +// repository or provides help to browsers that may have navigated to the vanity +// URL. The WithImport Option is mandatory since the go tool requires it to +// fetch the repository. +func Handler(opts ...Option) http.Handler { + return handlerFrom(compile(opts)) +} + +// Helpers for common VCSs. + +// Redirects gddo to browsable source files for GitHub hosted repositories. +func WithGitHubStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/tree/" + ref + "{/dir}" + file := repoPath + "/blob/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Redirects gddo to browsable source files for Gogs hosted repositories. +func WithGogsStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/src/" + ref + "{/dir}" + file := repoPath + "/src/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Creates a Handler that serves a GitHub repository at a specific importPath. +func GitHubHandler(importPath, user, repo, gitScheme string) http.Handler { + ghImportPath := "github.com/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+ghImportPath), + WithGitHubStyleSource(importPath, "https://"+ghImportPath, "master"), + ) +} + +// Creates a Handler that serves a repository hosted with Gogs at host at a +// specific importPath. +func GogsHandler(importPath, host, user, repo, gitScheme string) http.Handler { + gogsImportPath := host + "/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+gogsImportPath), + WithGogsStyleSource(importPath, "https://"+gogsImportPath, "master"), + ) +} diff --git a/branches/master/toyohime_test.go b/branches/master/toyohime_test.go new file mode 100644 index 0000000..bf6598f --- /dev/null +++ b/branches/master/toyohime_test.go @@ -0,0 +1,91 @@ +package toyohime + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGoTool(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/subpkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + h := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + h.ServeHTTP(res, req) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + + expected := `` + if !strings.Contains(string(body), expected) { + t.Fatalf("Expecting url '%v' body to contain html meta tag: '%v', but got:\n'%v'", test.path, expected, string(body)) + } + + expected = "text/html; charset=utf-8" + if res.HeaderMap.Get("content-type") != expected { + t.Fatalf("Expecting content type '%v', but got '%v'", expected, res.HeaderMap.Get("content-type")) + } + + if res.Code != http.StatusOK { + t.Fatalf("Expected response status 200, but got %v", res.Code) + } + } +} + +func TestBrowserGoDoc(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/sub/foo", "https://godocs.io/go.jonnrb.io/pkg/sub/foo"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + srv := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + srv.ServeHTTP(res, req) + + if res.Code != http.StatusTemporaryRedirect { + t.Fatalf("Expected response status %v, but got %v", http.StatusTemporaryRedirect, res.Code) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + if !strings.Contains(string(body), test.result) { + t.Fatalf("Expecting '%v' be contained in '%v'", test.result, string(body)) + } + } +} + +func ExampleGitHubHandler() { + // Redirects the vanity import path "go.jonnrb.io/vanity" to the code hosted + // on GitHub by user (or organization) "jonnrb" in repo "vanity" using the + // git over "https". + h := GitHubHandler("go.jonnrb.io/vanity", "jonnrb", "vanity", "https") + + http.Handle("/vanity", h) + http.Handle("/vanity/", h) // to handle requests for subpackages. + + http.ListenAndServe(":http", nil) +} diff --git a/branches/master/version.go b/branches/master/version.go new file mode 100644 index 0000000..81e847c --- /dev/null +++ b/branches/master/version.go @@ -0,0 +1,50 @@ +package toyohime + +import ( + "fmt" + "runtime/debug" + "strings" +) + +const ( + defaultVersion = "0.0.0" + defaultCommit = "HEAD" + defaultBuild = "0000-01-01:00:00+00:00" +) + +var ( + // Version is the tagged release version in the form .. + // following semantic versioning and is overwritten by the build system. + Version = defaultVersion + + // Commit is the commit sha of the build (normally from Git) and is overwritten + // by the build system. + Commit = defaultCommit + + // Build is the date and time of the build as an RFC3339 formatted string + // and is overwritten by the build system. + Build = defaultBuild +) + +// FullVersion display the full version and build +func FullVersion() string { + var sb strings.Builder + + isDefault := Version == defaultVersion && Commit == defaultCommit && Build == defaultBuild + + if !isDefault { + sb.WriteString(fmt.Sprintf("%s@%s %s", Version, Commit, Build)) + } + + if info, ok := debug.ReadBuildInfo(); ok { + if isDefault { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Version)) + } + sb.WriteString(fmt.Sprintf(" %s", info.GoVersion)) + if info.Main.Sum != "" { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Sum)) + } + } + + return sb.String() +} diff --git a/branches/origin-master/.gitignore b/branches/origin-master/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/branches/origin-master/LICENSE b/branches/origin-master/LICENSE new file mode 100644 index 0000000..d101e81 --- /dev/null +++ b/branches/origin-master/LICENSE @@ -0,0 +1,42 @@ +Copyright (c) 2018, Jon Betti +Copyright (c) 2023, Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +Copyright (c) 2016, Kare Nuorteva +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/branches/origin-master/Makefile b/branches/origin-master/Makefile new file mode 100644 index 0000000..903799b --- /dev/null +++ b/branches/origin-master/Makefile @@ -0,0 +1,26 @@ +GO ?= go +GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=$(VERSION) -X `go list`.Commit=$(COMMIT) -X `go list`.Build=$(BUILD)" -tags "static_build" +PREFIX ?= /usr/local + +VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"` +COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"` +BRANCH = `git rev-parse --abbrev-ref HEAD` +BUILD = `git show -s --pretty=format:%cI` + +GOARCH ?= amd64 +GOOS ?= linux + +all: toyohime + +toyohime: + ${GO} build ${GOFLAGS} ./cmd/toyohime +clean: + rm toyohime +install: + install -m0755 toyohime ${DESTDIR}${PREFIX}/bin/yorihime + install -Dm0044 toyohime.1 ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 +test: + ${GO} test . +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/toyohime + rm -f ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 diff --git a/branches/origin-master/README.md b/branches/origin-master/README.md new file mode 100644 index 0000000..08a2111 --- /dev/null +++ b/branches/origin-master/README.md @@ -0,0 +1,25 @@ +# Toyohime +Fork of [go.jonnrb.io/vanity](https://go.jonnrb.io/vanity) + +A vanity import path is any import path that can be downloaded with +`go get` but isn't otherwise blessed by the `go` tool (e.g. GitHub, +BitBucket, etc.). A commonly used vanity import path is +"golang.org/x/...". This package attempts to mimic the behavior of +"golang.org/x/..." as closely as possible. + +## Features + +* Redirects browsers to godocs.io (or somewhere else) +* Redirects Go tool to VCS +* Redirects pkg.go.dev to browsable files + +## Installation + +```bash +go install marisa.chaotic.ninja/toyohime/cmd/toyohime@latest +``` + +## Specification + - [Remote Import Paths](https://golang.org/cmd/go/#hdr-Remote_import_paths) + - [GDDO Source Code Links](https://github.com/golang/gddo/wiki/Source-Code-Links) + - [Custom Import Path Checking](https://docs.google.com/document/d/1jVFkZTcYbNLaTxXD9OcGfn7vYv5hWtPx9--lTx1gPMs/edit) diff --git a/branches/origin-master/cmd/toyohime/README.md b/branches/origin-master/cmd/toyohime/README.md new file mode 100644 index 0000000..0bf7f81 --- /dev/null +++ b/branches/origin-master/cmd/toyohime/README.md @@ -0,0 +1,21 @@ +# Toyohime (command) + +Runs a barebones vanity server over HTTP. + +## Usage + +``` +./toyohime [-index] fqdn [repo file] +``` + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +If repo file is not given, "./repos" is used. The file has the following format: + +``` +pkgroot vcsScheme://vcsHost/user/repo +pkgroot2 vcsScheme://vcsHost/user/repo2 +``` + +vcsHost is either a [Gogs](https://gogs.io) server (that's what I use) or [GitHub](https://github.com). I'm open to supporting other VCSs but I'm not sure what that would look like. diff --git a/branches/origin-master/cmd/toyohime/dynamic_handler.go b/branches/origin-master/cmd/toyohime/dynamic_handler.go new file mode 100644 index 0000000..9087230 --- /dev/null +++ b/branches/origin-master/cmd/toyohime/dynamic_handler.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + "net/http" + "sync" + + "github.com/fsnotify/fsnotify" +) + +type dynamicHandler struct { + *fsnotify.Watcher + + h http.Handler + unhealthy bool + mu sync.RWMutex +} + +func newDynamicHandler(file string, generator func() (http.Handler, error)) *dynamicHandler { + w, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("Failed to create fsnotify.Watcher: %v", err) + } + + if err := w.Add(file); err != nil { + log.Fatalf("Could not watch file %q: %v", file, err) + } + + h, err := generator() + if err != nil { + log.Fatalf("Failed generating initial handler: %v", err) + } + + dh := &dynamicHandler{Watcher: w, h: h} + updateHandler := func(h http.Handler, err error) error { + dh.mu.Lock() + defer dh.mu.Unlock() + + if err != nil { + dh.unhealthy = true + return err + } + dh.unhealthy = false + + if h == nil { + panic("nil handler returned from generator") + } + dh.h = h + return nil + } + + go func() { + log.Printf("Watching for changes to %q", file) + for { + select { + case evt, ok := <-w.Events: + if !ok { + return + } + if evt.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename) == 0 { + continue + } + if err := updateHandler(generator()); err != nil { + log.Printf("Error switching to new handler: %v", err) + } else { + log.Printf("Updated to new handler based on %q", file) + } + case err, ok := <-w.Errors: + if !ok { + return + } + log.Printf("Error in fsnotify.Watcher: %v", err) + } + } + }() + + return dh +} + +func (dh *dynamicHandler) IsHealthy() bool { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return !dh.unhealthy +} + +func (dh *dynamicHandler) getHandler() http.Handler { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return dh.h +} + +func (dh *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dh.getHandler().ServeHTTP(w, r) +} diff --git a/branches/origin-master/cmd/toyohime/main.go b/branches/origin-master/cmd/toyohime/main.go new file mode 100644 index 0000000..2204d40 --- /dev/null +++ b/branches/origin-master/cmd/toyohime/main.go @@ -0,0 +1,282 @@ +/* +Runs a barebones vanity server over HTTP. + +Usage + + ./toyohime [-index] [-nohealthz] fqdn [repo file] + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +The "-nohealthz" flag disables the "/healthz" endpoint that returns a 200 OK +when everything is OK. + +The "-watch" flag watches the repo file for changes. When it is updated, the +updated version will be used for serving. + +If repo file is not given, "./repos" is used. The file has the following format: + + pkgroot vcsScheme://vcsHost/user/repo + pkgroot2 vcsScheme://vcsHost/user/repo2 + +vcsHost is either a Gogs server (that's what I use) or github.com. I'm open to +supporting other VCSs but I'm not sure what that would look like. +*/ +package main // go.jonnrb.io/vanity + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "html/template" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "marisa.chaotic.ninja/toyohime" +) + +var ( + showIndex = flag.Bool("index", false, "Show a list of repos at /") + noHealthz = flag.Bool("nohealthz", false, "Disable healthcheck endpoint at /healthz") + watch = flag.Bool("watch", false, "Watch repos file for changes and reload") + listenPort = flag.String("listen", ":8080", "Port to bind on") +) + +var ( + host string // param 1 + reposPath string = "repos" // param 2 +) + +func serveRepo(mux *http.ServeMux, root string, u *url.URL) { + vcsScheme, vcsHost := u.Scheme, u.Host + + // Get ["", "user", "repo"]. + pathParts := strings.Split(u.Path, "/") + if len(pathParts) != 3 { + log.Fatalf("Repo URL must be of the form vcsScheme://vcsHost/user/repo but got %q", u.String()) + } + user, repo := pathParts[1], pathParts[2] + + importPath := host + "/" + root + var h http.Handler + if vcsHost == "github.com" { + h = toyohime.GitHubHandler(importPath, user, repo, vcsScheme) + } else { + h = toyohime.GogsHandler(importPath, vcsHost, user, repo, vcsScheme) + } + mux.Handle("/"+root, h) + mux.Handle("/"+root+"/", h) +} + +func addRepoHandlers(mux *http.ServeMux, r io.Reader) error { + indexMap := map[string]string{} + + sc := bufio.NewScanner(r) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + switch len(fields) { + case 0: + continue + case 2: + // Pass + default: + return fmt.Errorf("expected line of form \"path vcsScheme://vcsHost/user/repo\" but got %q", sc.Text()) + } + + if *showIndex { + indexMap[fields[0]] = fields[1] + } + + path := fields[0] + u, err := url.Parse(fields[1]) + if err != nil { + return fmt.Errorf("repo was not a valid URL: %q", fields[1]) + } + + serveRepo(mux, path, u) + } + + if !*showIndex { + return nil + } + + var b bytes.Buffer + err := template.Must(template.New("").Parse(` + + + +Import paths hosted at {{ .Host }} + + + +{{ $host := .Host }} +
+

でホストされているインポート パス {{ html $host }}

+
+
+ +{{ range $root, $repo := .IndexMap }} + +
+
+* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +
+
+
+

{{ html $root }}

+Package
+Repository +{{ else }} +Nothing here. +{{ end }} +
+
+

~から明らかに盗まれた azukifont.com

+
+ + +`)).Execute(&b, struct { + IndexMap map[string]string + Host string + }{ + IndexMap: indexMap, + Host: host, + }) + if err != nil { + return fmt.Errorf("couldn't create index page: %v", err) + } + buf := b.Bytes() + + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + io.Copy(w, bytes.NewReader(buf)) + })) + return nil +} + +func registerHealthz(mux *http.ServeMux, isHealthy func() bool) { + mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isHealthy() { + io.WriteString(w, "OK\r\n") + } else { + http.Error(w, "internal error\r\n", http.StatusInternalServerError) + } + })) +} + +var healthcheck = func() bool { + return true +} + +func generateHandler() (http.Handler, error) { + mux := http.NewServeMux() + + f, err := os.Open(reposPath) + if err != nil { + return nil, fmt.Errorf("error opening %q: %v", reposPath, err) + } + if err := addRepoHandlers(mux, f); err != nil { + return nil, err + } + + if !*noHealthz { + registerHealthz(mux, healthcheck) + } + return mux, nil +} + +func buildServer(h http.Handler) *http.Server { + return &http.Server{ + // This should be sufficient. + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 5 * time.Second, + + Addr: *listenPort, + Handler: h, + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: %s fqdn [repos file]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + host = flag.Arg(0) + if host == "" { + flag.Usage() + os.Exit(-1) + } + + if override := flag.Arg(1); override != "" { + reposPath = override + } + + var h http.Handler + if *watch { + dh := newDynamicHandler(reposPath, generateHandler) + healthcheck = dh.IsHealthy + defer dh.Close() + h = dh + } else { + var err error + h, err = generateHandler() + if err != nil { + log.Printf("Error generating handler: %v", err) + } + } + + srv := buildServer(h) + + log.Printf("starting toyohime %v on port %v\n", toyohime.FullVersion(), *listenPort) + log.Println(srv.ListenAndServe()) +} diff --git a/branches/origin-master/go.mod b/branches/origin-master/go.mod new file mode 100644 index 0000000..e2712e7 --- /dev/null +++ b/branches/origin-master/go.mod @@ -0,0 +1,5 @@ +module marisa.chaotic.ninja/toyohime + +go 1.14 + +require github.com/fsnotify/fsnotify v1.4.9 diff --git a/branches/origin-master/go.sum b/branches/origin-master/go.sum new file mode 100644 index 0000000..b12c1ed --- /dev/null +++ b/branches/origin-master/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/branches/origin-master/rc.d/freebsd.sh b/branches/origin-master/rc.d/freebsd.sh new file mode 100644 index 0000000..005a6e6 --- /dev/null +++ b/branches/origin-master/rc.d/freebsd.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON NETWORKING +# BEFORE: LOGIN +# KEYWORD: shutdown + +. /etc/rc.subr + +name="toyohime" +rcvar="${name}_enable" + +: ${toyohime_enable="NO"} +: ${toyohime_fqdn="localhost"} +: ${toyohime_user="www"} +: ${toyohime_group="www"} +: ${toyohime_repos="/usr/local/etc/toyohime-repos"} +: ${toyohime_address="127.0.0.1:8080"} + +command="/usr/sbin/daemon" +pidfile="/var/run/${name}.pid" +command_args="-p ${pidfile} -u ${toyohime_user} /usr/local/bin/${name} -listen ${toyohime_address} -index -watch ${toyohime_fqdn} ${toyohime_repos}" + +load_rc_config "${name}" +run_rc_command "$1" diff --git a/branches/origin-master/rc.d/immortal.yml b/branches/origin-master/rc.d/immortal.yml new file mode 100644 index 0000000..7e0008b --- /dev/null +++ b/branches/origin-master/rc.d/immortal.yml @@ -0,0 +1,2 @@ +cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos +user: www diff --git a/branches/origin-master/rc.d/netbsd.sh b/branches/origin-master/rc.d/netbsd.sh new file mode 100644 index 0000000..8c9cdd6 --- /dev/null +++ b/branches/origin-master/rc.d/netbsd.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON +# BEFORE: LOGIN +# KEYWORD: shutdown + +$rc_subr_loaded . /etc/rc.subr + +name="toyohime" +rcvar="$name" +command="/usr/local/bin/$name" +command_args="-listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos" + +load_rc_config "$name" +run_rc_command "$1" diff --git a/branches/origin-master/rc.d/openbsd.ksh b/branches/origin-master/rc.d/openbsd.ksh new file mode 100644 index 0000000..5c56be0 --- /dev/null +++ b/branches/origin-master/rc.d/openbsd.ksh @@ -0,0 +1,11 @@ +#!/bin/ksh +# $TheSupernovaDuo$ + +. /etc/rc.d/rc.subr + +daemon="/usr/local/bin/toyohime" +daemon_flags="-listen 127.0.0.1:8080 -index localhost /etc/toyohime-repos" +daemon_user="www" +rc_bg=YES + +rc_cmd "$1" diff --git a/branches/origin-master/toyohime.1 b/branches/origin-master/toyohime.1 new file mode 100644 index 0000000..7ba383f --- /dev/null +++ b/branches/origin-master/toyohime.1 @@ -0,0 +1,25 @@ +.Dd $Mdocdate$ +.Dt TOYOHIME 1 +.Os +.Sh NAME +.Nm toyohime +.Nd Library and CLI for hosting custom vanity URIs for the Go tool +.Sh SYNOPSIS +.Nm +.Op Fl index +.Op Fl listen Ar ip:port +.Op Fl nohealthz +.Op Fl watch +.Op Ar fqdn +.Op Ar path/to/repos/file +.Sh DESCRIPTION +.Nm +is a library and command line implementation +that allows developers to have their own path +for their Go packages, closely replicating +the behavior of golang.org/x/.. as much as +possible. +.Sh AUTHORS +.An Jon Betti Aq Mt jonbetti@gmail.com +.Sh MAINTAINERS +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/branches/origin-master/toyohime.go b/branches/origin-master/toyohime.go new file mode 100644 index 0000000..0439d4d --- /dev/null +++ b/branches/origin-master/toyohime.go @@ -0,0 +1,179 @@ +/* +Package toyohime implements custom import paths (Go vanity URLs) as an HTTP +handler that can be installed at the vanity URL. +*/ +package toyohime // import "go.jonnrb.io/vanity" <- original one for reference + +import ( + "fmt" + "html/template" + "net/http" + "strings" +) + +type config struct { + importTag *string + sourceTag *string + redir Redirector +} + +// Configures the Handler. The only required option is WithImport. +type Option func(*config) + +// Instructs the go tool where to fetch the repo at vcsRoot and the importPath +// that tree should be rooted at. +func WithImport(importPath, vcs, vcsRoot string) Option { + importTag := "" + return func(cfg *config) { + if cfg.importTag != nil { + panic(fmt.Sprintf("vanity: existing import tag: %s", *cfg.importTag)) + } + cfg.importTag = &importTag + } +} + +// Instructs gddo (godoc.org) how to direct browsers to browsable source code +// for packages and their contents rooted at prefix. +// +// home specifies the home page of prefix, directory gives a format for how to +// browse a directory, and file gives a format for how to view a file and go to +// specific lines within it. +// +// More information can be found at https://github.com/golang/gddo/wiki/Source-Code-Links. +// +func WithSource(prefix, home, directory, file string) Option { + sourceTag := "" + return func(cfg *config) { + if cfg.sourceTag != nil { + panic(fmt.Sprintf("vanity: existing source tag: %s", *cfg.importTag)) + } + cfg.sourceTag = &sourceTag + } +} + +// When a browser navigates to the vanity URL of pkg, this function rewrites +// pkg to a browsable URL. +type Redirector func(pkg string) (url string) + +func WithRedirector(redir Redirector) Option { + return func(cfg *config) { + if cfg.redir != nil { + panic("vanity: existing Redirector") + } + cfg.redir = redir + } +} + +func compile(opts []Option) (*template.Template, Redirector) { + // Process options. + var cfg config + for _, opt := range opts { + opt(&cfg) + } + + // A WithImport is required. + if cfg.importTag == nil { + panic("vanity: WithImport is required") + } + + tags := []string{*cfg.importTag} + if cfg.sourceTag != nil { + tags = append(tags, *cfg.sourceTag) + } + tagBlk := strings.Join(tags, "\n") + + h := fmt.Sprintf(` + + + +%s + + + +Nothing to see here; move along. + + +`, tagBlk) + + // Use default GDDO Redirector. + if cfg.redir == nil { + cfg.redir = func(pkg string) string { + return "https://pkg.go.dev/" + pkg + } + } + + return template.Must(template.New("").Parse(h)), cfg.redir +} + +func handlerFrom(tpl *template.Template, redir Redirector) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only method supported is GET. + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(w, http.StatusText(status), status) + return + } + + pkg := r.Host + r.URL.Path + redirURL := redir(pkg) + + // Issue an HTTP redirect if this is definitely a browser. + if r.FormValue("go-get") != "1" { + http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + if err := tpl.ExecuteTemplate(w, "", redirURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + +// Returns an http.Handler that serves the vanity URL information for a single +// repository. Each Option gives additional information to agents about the +// repository or provides help to browsers that may have navigated to the vanity +// URL. The WithImport Option is mandatory since the go tool requires it to +// fetch the repository. +func Handler(opts ...Option) http.Handler { + return handlerFrom(compile(opts)) +} + +// Helpers for common VCSs. + +// Redirects gddo to browsable source files for GitHub hosted repositories. +func WithGitHubStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/tree/" + ref + "{/dir}" + file := repoPath + "/blob/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Redirects gddo to browsable source files for Gogs hosted repositories. +func WithGogsStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/src/" + ref + "{/dir}" + file := repoPath + "/src/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Creates a Handler that serves a GitHub repository at a specific importPath. +func GitHubHandler(importPath, user, repo, gitScheme string) http.Handler { + ghImportPath := "github.com/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+ghImportPath), + WithGitHubStyleSource(importPath, "https://"+ghImportPath, "master"), + ) +} + +// Creates a Handler that serves a repository hosted with Gogs at host at a +// specific importPath. +func GogsHandler(importPath, host, user, repo, gitScheme string) http.Handler { + gogsImportPath := host + "/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+gogsImportPath), + WithGogsStyleSource(importPath, "https://"+gogsImportPath, "master"), + ) +} diff --git a/branches/origin-master/toyohime_test.go b/branches/origin-master/toyohime_test.go new file mode 100644 index 0000000..bf6598f --- /dev/null +++ b/branches/origin-master/toyohime_test.go @@ -0,0 +1,91 @@ +package toyohime + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGoTool(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/subpkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + h := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + h.ServeHTTP(res, req) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + + expected := `` + if !strings.Contains(string(body), expected) { + t.Fatalf("Expecting url '%v' body to contain html meta tag: '%v', but got:\n'%v'", test.path, expected, string(body)) + } + + expected = "text/html; charset=utf-8" + if res.HeaderMap.Get("content-type") != expected { + t.Fatalf("Expecting content type '%v', but got '%v'", expected, res.HeaderMap.Get("content-type")) + } + + if res.Code != http.StatusOK { + t.Fatalf("Expected response status 200, but got %v", res.Code) + } + } +} + +func TestBrowserGoDoc(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/sub/foo", "https://godocs.io/go.jonnrb.io/pkg/sub/foo"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + srv := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + srv.ServeHTTP(res, req) + + if res.Code != http.StatusTemporaryRedirect { + t.Fatalf("Expected response status %v, but got %v", http.StatusTemporaryRedirect, res.Code) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + if !strings.Contains(string(body), test.result) { + t.Fatalf("Expecting '%v' be contained in '%v'", test.result, string(body)) + } + } +} + +func ExampleGitHubHandler() { + // Redirects the vanity import path "go.jonnrb.io/vanity" to the code hosted + // on GitHub by user (or organization) "jonnrb" in repo "vanity" using the + // git over "https". + h := GitHubHandler("go.jonnrb.io/vanity", "jonnrb", "vanity", "https") + + http.Handle("/vanity", h) + http.Handle("/vanity/", h) // to handle requests for subpackages. + + http.ListenAndServe(":http", nil) +} diff --git a/branches/origin-master/version.go b/branches/origin-master/version.go new file mode 100644 index 0000000..81e847c --- /dev/null +++ b/branches/origin-master/version.go @@ -0,0 +1,50 @@ +package toyohime + +import ( + "fmt" + "runtime/debug" + "strings" +) + +const ( + defaultVersion = "0.0.0" + defaultCommit = "HEAD" + defaultBuild = "0000-01-01:00:00+00:00" +) + +var ( + // Version is the tagged release version in the form .. + // following semantic versioning and is overwritten by the build system. + Version = defaultVersion + + // Commit is the commit sha of the build (normally from Git) and is overwritten + // by the build system. + Commit = defaultCommit + + // Build is the date and time of the build as an RFC3339 formatted string + // and is overwritten by the build system. + Build = defaultBuild +) + +// FullVersion display the full version and build +func FullVersion() string { + var sb strings.Builder + + isDefault := Version == defaultVersion && Commit == defaultCommit && Build == defaultBuild + + if !isDefault { + sb.WriteString(fmt.Sprintf("%s@%s %s", Version, Commit, Build)) + } + + if info, ok := debug.ReadBuildInfo(); ok { + if isDefault { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Version)) + } + sb.WriteString(fmt.Sprintf(" %s", info.GoVersion)) + if info.Main.Sum != "" { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Sum)) + } + } + + return sb.String() +} diff --git a/branches/origin/.gitignore b/branches/origin/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/branches/origin/LICENSE b/branches/origin/LICENSE new file mode 100644 index 0000000..d101e81 --- /dev/null +++ b/branches/origin/LICENSE @@ -0,0 +1,42 @@ +Copyright (c) 2018, Jon Betti +Copyright (c) 2023, Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +Copyright (c) 2016, Kare Nuorteva +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/branches/origin/Makefile b/branches/origin/Makefile new file mode 100644 index 0000000..903799b --- /dev/null +++ b/branches/origin/Makefile @@ -0,0 +1,26 @@ +GO ?= go +GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=$(VERSION) -X `go list`.Commit=$(COMMIT) -X `go list`.Build=$(BUILD)" -tags "static_build" +PREFIX ?= /usr/local + +VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"` +COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"` +BRANCH = `git rev-parse --abbrev-ref HEAD` +BUILD = `git show -s --pretty=format:%cI` + +GOARCH ?= amd64 +GOOS ?= linux + +all: toyohime + +toyohime: + ${GO} build ${GOFLAGS} ./cmd/toyohime +clean: + rm toyohime +install: + install -m0755 toyohime ${DESTDIR}${PREFIX}/bin/yorihime + install -Dm0044 toyohime.1 ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 +test: + ${GO} test . +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/toyohime + rm -f ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 diff --git a/branches/origin/README.md b/branches/origin/README.md new file mode 100644 index 0000000..08a2111 --- /dev/null +++ b/branches/origin/README.md @@ -0,0 +1,25 @@ +# Toyohime +Fork of [go.jonnrb.io/vanity](https://go.jonnrb.io/vanity) + +A vanity import path is any import path that can be downloaded with +`go get` but isn't otherwise blessed by the `go` tool (e.g. GitHub, +BitBucket, etc.). A commonly used vanity import path is +"golang.org/x/...". This package attempts to mimic the behavior of +"golang.org/x/..." as closely as possible. + +## Features + +* Redirects browsers to godocs.io (or somewhere else) +* Redirects Go tool to VCS +* Redirects pkg.go.dev to browsable files + +## Installation + +```bash +go install marisa.chaotic.ninja/toyohime/cmd/toyohime@latest +``` + +## Specification + - [Remote Import Paths](https://golang.org/cmd/go/#hdr-Remote_import_paths) + - [GDDO Source Code Links](https://github.com/golang/gddo/wiki/Source-Code-Links) + - [Custom Import Path Checking](https://docs.google.com/document/d/1jVFkZTcYbNLaTxXD9OcGfn7vYv5hWtPx9--lTx1gPMs/edit) diff --git a/branches/origin/cmd/toyohime/README.md b/branches/origin/cmd/toyohime/README.md new file mode 100644 index 0000000..0bf7f81 --- /dev/null +++ b/branches/origin/cmd/toyohime/README.md @@ -0,0 +1,21 @@ +# Toyohime (command) + +Runs a barebones vanity server over HTTP. + +## Usage + +``` +./toyohime [-index] fqdn [repo file] +``` + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +If repo file is not given, "./repos" is used. The file has the following format: + +``` +pkgroot vcsScheme://vcsHost/user/repo +pkgroot2 vcsScheme://vcsHost/user/repo2 +``` + +vcsHost is either a [Gogs](https://gogs.io) server (that's what I use) or [GitHub](https://github.com). I'm open to supporting other VCSs but I'm not sure what that would look like. diff --git a/branches/origin/cmd/toyohime/dynamic_handler.go b/branches/origin/cmd/toyohime/dynamic_handler.go new file mode 100644 index 0000000..9087230 --- /dev/null +++ b/branches/origin/cmd/toyohime/dynamic_handler.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + "net/http" + "sync" + + "github.com/fsnotify/fsnotify" +) + +type dynamicHandler struct { + *fsnotify.Watcher + + h http.Handler + unhealthy bool + mu sync.RWMutex +} + +func newDynamicHandler(file string, generator func() (http.Handler, error)) *dynamicHandler { + w, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("Failed to create fsnotify.Watcher: %v", err) + } + + if err := w.Add(file); err != nil { + log.Fatalf("Could not watch file %q: %v", file, err) + } + + h, err := generator() + if err != nil { + log.Fatalf("Failed generating initial handler: %v", err) + } + + dh := &dynamicHandler{Watcher: w, h: h} + updateHandler := func(h http.Handler, err error) error { + dh.mu.Lock() + defer dh.mu.Unlock() + + if err != nil { + dh.unhealthy = true + return err + } + dh.unhealthy = false + + if h == nil { + panic("nil handler returned from generator") + } + dh.h = h + return nil + } + + go func() { + log.Printf("Watching for changes to %q", file) + for { + select { + case evt, ok := <-w.Events: + if !ok { + return + } + if evt.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename) == 0 { + continue + } + if err := updateHandler(generator()); err != nil { + log.Printf("Error switching to new handler: %v", err) + } else { + log.Printf("Updated to new handler based on %q", file) + } + case err, ok := <-w.Errors: + if !ok { + return + } + log.Printf("Error in fsnotify.Watcher: %v", err) + } + } + }() + + return dh +} + +func (dh *dynamicHandler) IsHealthy() bool { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return !dh.unhealthy +} + +func (dh *dynamicHandler) getHandler() http.Handler { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return dh.h +} + +func (dh *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dh.getHandler().ServeHTTP(w, r) +} diff --git a/branches/origin/cmd/toyohime/main.go b/branches/origin/cmd/toyohime/main.go new file mode 100644 index 0000000..2204d40 --- /dev/null +++ b/branches/origin/cmd/toyohime/main.go @@ -0,0 +1,282 @@ +/* +Runs a barebones vanity server over HTTP. + +Usage + + ./toyohime [-index] [-nohealthz] fqdn [repo file] + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +The "-nohealthz" flag disables the "/healthz" endpoint that returns a 200 OK +when everything is OK. + +The "-watch" flag watches the repo file for changes. When it is updated, the +updated version will be used for serving. + +If repo file is not given, "./repos" is used. The file has the following format: + + pkgroot vcsScheme://vcsHost/user/repo + pkgroot2 vcsScheme://vcsHost/user/repo2 + +vcsHost is either a Gogs server (that's what I use) or github.com. I'm open to +supporting other VCSs but I'm not sure what that would look like. +*/ +package main // go.jonnrb.io/vanity + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "html/template" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "marisa.chaotic.ninja/toyohime" +) + +var ( + showIndex = flag.Bool("index", false, "Show a list of repos at /") + noHealthz = flag.Bool("nohealthz", false, "Disable healthcheck endpoint at /healthz") + watch = flag.Bool("watch", false, "Watch repos file for changes and reload") + listenPort = flag.String("listen", ":8080", "Port to bind on") +) + +var ( + host string // param 1 + reposPath string = "repos" // param 2 +) + +func serveRepo(mux *http.ServeMux, root string, u *url.URL) { + vcsScheme, vcsHost := u.Scheme, u.Host + + // Get ["", "user", "repo"]. + pathParts := strings.Split(u.Path, "/") + if len(pathParts) != 3 { + log.Fatalf("Repo URL must be of the form vcsScheme://vcsHost/user/repo but got %q", u.String()) + } + user, repo := pathParts[1], pathParts[2] + + importPath := host + "/" + root + var h http.Handler + if vcsHost == "github.com" { + h = toyohime.GitHubHandler(importPath, user, repo, vcsScheme) + } else { + h = toyohime.GogsHandler(importPath, vcsHost, user, repo, vcsScheme) + } + mux.Handle("/"+root, h) + mux.Handle("/"+root+"/", h) +} + +func addRepoHandlers(mux *http.ServeMux, r io.Reader) error { + indexMap := map[string]string{} + + sc := bufio.NewScanner(r) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + switch len(fields) { + case 0: + continue + case 2: + // Pass + default: + return fmt.Errorf("expected line of form \"path vcsScheme://vcsHost/user/repo\" but got %q", sc.Text()) + } + + if *showIndex { + indexMap[fields[0]] = fields[1] + } + + path := fields[0] + u, err := url.Parse(fields[1]) + if err != nil { + return fmt.Errorf("repo was not a valid URL: %q", fields[1]) + } + + serveRepo(mux, path, u) + } + + if !*showIndex { + return nil + } + + var b bytes.Buffer + err := template.Must(template.New("").Parse(` + + + +Import paths hosted at {{ .Host }} + + + +{{ $host := .Host }} +
+

でホストされているインポート パス {{ html $host }}

+
+
+ +{{ range $root, $repo := .IndexMap }} + +
+
+* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +
+
+
+

{{ html $root }}

+Package
+Repository +{{ else }} +Nothing here. +{{ end }} +
+
+

~から明らかに盗まれた azukifont.com

+
+ + +`)).Execute(&b, struct { + IndexMap map[string]string + Host string + }{ + IndexMap: indexMap, + Host: host, + }) + if err != nil { + return fmt.Errorf("couldn't create index page: %v", err) + } + buf := b.Bytes() + + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + io.Copy(w, bytes.NewReader(buf)) + })) + return nil +} + +func registerHealthz(mux *http.ServeMux, isHealthy func() bool) { + mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isHealthy() { + io.WriteString(w, "OK\r\n") + } else { + http.Error(w, "internal error\r\n", http.StatusInternalServerError) + } + })) +} + +var healthcheck = func() bool { + return true +} + +func generateHandler() (http.Handler, error) { + mux := http.NewServeMux() + + f, err := os.Open(reposPath) + if err != nil { + return nil, fmt.Errorf("error opening %q: %v", reposPath, err) + } + if err := addRepoHandlers(mux, f); err != nil { + return nil, err + } + + if !*noHealthz { + registerHealthz(mux, healthcheck) + } + return mux, nil +} + +func buildServer(h http.Handler) *http.Server { + return &http.Server{ + // This should be sufficient. + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 5 * time.Second, + + Addr: *listenPort, + Handler: h, + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: %s fqdn [repos file]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + host = flag.Arg(0) + if host == "" { + flag.Usage() + os.Exit(-1) + } + + if override := flag.Arg(1); override != "" { + reposPath = override + } + + var h http.Handler + if *watch { + dh := newDynamicHandler(reposPath, generateHandler) + healthcheck = dh.IsHealthy + defer dh.Close() + h = dh + } else { + var err error + h, err = generateHandler() + if err != nil { + log.Printf("Error generating handler: %v", err) + } + } + + srv := buildServer(h) + + log.Printf("starting toyohime %v on port %v\n", toyohime.FullVersion(), *listenPort) + log.Println(srv.ListenAndServe()) +} diff --git a/branches/origin/go.mod b/branches/origin/go.mod new file mode 100644 index 0000000..e2712e7 --- /dev/null +++ b/branches/origin/go.mod @@ -0,0 +1,5 @@ +module marisa.chaotic.ninja/toyohime + +go 1.14 + +require github.com/fsnotify/fsnotify v1.4.9 diff --git a/branches/origin/go.sum b/branches/origin/go.sum new file mode 100644 index 0000000..b12c1ed --- /dev/null +++ b/branches/origin/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/branches/origin/rc.d/freebsd.sh b/branches/origin/rc.d/freebsd.sh new file mode 100644 index 0000000..005a6e6 --- /dev/null +++ b/branches/origin/rc.d/freebsd.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON NETWORKING +# BEFORE: LOGIN +# KEYWORD: shutdown + +. /etc/rc.subr + +name="toyohime" +rcvar="${name}_enable" + +: ${toyohime_enable="NO"} +: ${toyohime_fqdn="localhost"} +: ${toyohime_user="www"} +: ${toyohime_group="www"} +: ${toyohime_repos="/usr/local/etc/toyohime-repos"} +: ${toyohime_address="127.0.0.1:8080"} + +command="/usr/sbin/daemon" +pidfile="/var/run/${name}.pid" +command_args="-p ${pidfile} -u ${toyohime_user} /usr/local/bin/${name} -listen ${toyohime_address} -index -watch ${toyohime_fqdn} ${toyohime_repos}" + +load_rc_config "${name}" +run_rc_command "$1" diff --git a/branches/origin/rc.d/immortal.yml b/branches/origin/rc.d/immortal.yml new file mode 100644 index 0000000..7e0008b --- /dev/null +++ b/branches/origin/rc.d/immortal.yml @@ -0,0 +1,2 @@ +cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos +user: www diff --git a/branches/origin/rc.d/netbsd.sh b/branches/origin/rc.d/netbsd.sh new file mode 100644 index 0000000..8c9cdd6 --- /dev/null +++ b/branches/origin/rc.d/netbsd.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON +# BEFORE: LOGIN +# KEYWORD: shutdown + +$rc_subr_loaded . /etc/rc.subr + +name="toyohime" +rcvar="$name" +command="/usr/local/bin/$name" +command_args="-listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos" + +load_rc_config "$name" +run_rc_command "$1" diff --git a/branches/origin/rc.d/openbsd.ksh b/branches/origin/rc.d/openbsd.ksh new file mode 100644 index 0000000..5c56be0 --- /dev/null +++ b/branches/origin/rc.d/openbsd.ksh @@ -0,0 +1,11 @@ +#!/bin/ksh +# $TheSupernovaDuo$ + +. /etc/rc.d/rc.subr + +daemon="/usr/local/bin/toyohime" +daemon_flags="-listen 127.0.0.1:8080 -index localhost /etc/toyohime-repos" +daemon_user="www" +rc_bg=YES + +rc_cmd "$1" diff --git a/branches/origin/toyohime.1 b/branches/origin/toyohime.1 new file mode 100644 index 0000000..7ba383f --- /dev/null +++ b/branches/origin/toyohime.1 @@ -0,0 +1,25 @@ +.Dd $Mdocdate$ +.Dt TOYOHIME 1 +.Os +.Sh NAME +.Nm toyohime +.Nd Library and CLI for hosting custom vanity URIs for the Go tool +.Sh SYNOPSIS +.Nm +.Op Fl index +.Op Fl listen Ar ip:port +.Op Fl nohealthz +.Op Fl watch +.Op Ar fqdn +.Op Ar path/to/repos/file +.Sh DESCRIPTION +.Nm +is a library and command line implementation +that allows developers to have their own path +for their Go packages, closely replicating +the behavior of golang.org/x/.. as much as +possible. +.Sh AUTHORS +.An Jon Betti Aq Mt jonbetti@gmail.com +.Sh MAINTAINERS +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/branches/origin/toyohime.go b/branches/origin/toyohime.go new file mode 100644 index 0000000..0439d4d --- /dev/null +++ b/branches/origin/toyohime.go @@ -0,0 +1,179 @@ +/* +Package toyohime implements custom import paths (Go vanity URLs) as an HTTP +handler that can be installed at the vanity URL. +*/ +package toyohime // import "go.jonnrb.io/vanity" <- original one for reference + +import ( + "fmt" + "html/template" + "net/http" + "strings" +) + +type config struct { + importTag *string + sourceTag *string + redir Redirector +} + +// Configures the Handler. The only required option is WithImport. +type Option func(*config) + +// Instructs the go tool where to fetch the repo at vcsRoot and the importPath +// that tree should be rooted at. +func WithImport(importPath, vcs, vcsRoot string) Option { + importTag := "" + return func(cfg *config) { + if cfg.importTag != nil { + panic(fmt.Sprintf("vanity: existing import tag: %s", *cfg.importTag)) + } + cfg.importTag = &importTag + } +} + +// Instructs gddo (godoc.org) how to direct browsers to browsable source code +// for packages and their contents rooted at prefix. +// +// home specifies the home page of prefix, directory gives a format for how to +// browse a directory, and file gives a format for how to view a file and go to +// specific lines within it. +// +// More information can be found at https://github.com/golang/gddo/wiki/Source-Code-Links. +// +func WithSource(prefix, home, directory, file string) Option { + sourceTag := "" + return func(cfg *config) { + if cfg.sourceTag != nil { + panic(fmt.Sprintf("vanity: existing source tag: %s", *cfg.importTag)) + } + cfg.sourceTag = &sourceTag + } +} + +// When a browser navigates to the vanity URL of pkg, this function rewrites +// pkg to a browsable URL. +type Redirector func(pkg string) (url string) + +func WithRedirector(redir Redirector) Option { + return func(cfg *config) { + if cfg.redir != nil { + panic("vanity: existing Redirector") + } + cfg.redir = redir + } +} + +func compile(opts []Option) (*template.Template, Redirector) { + // Process options. + var cfg config + for _, opt := range opts { + opt(&cfg) + } + + // A WithImport is required. + if cfg.importTag == nil { + panic("vanity: WithImport is required") + } + + tags := []string{*cfg.importTag} + if cfg.sourceTag != nil { + tags = append(tags, *cfg.sourceTag) + } + tagBlk := strings.Join(tags, "\n") + + h := fmt.Sprintf(` + + + +%s + + + +Nothing to see here; move along. + + +`, tagBlk) + + // Use default GDDO Redirector. + if cfg.redir == nil { + cfg.redir = func(pkg string) string { + return "https://pkg.go.dev/" + pkg + } + } + + return template.Must(template.New("").Parse(h)), cfg.redir +} + +func handlerFrom(tpl *template.Template, redir Redirector) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only method supported is GET. + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(w, http.StatusText(status), status) + return + } + + pkg := r.Host + r.URL.Path + redirURL := redir(pkg) + + // Issue an HTTP redirect if this is definitely a browser. + if r.FormValue("go-get") != "1" { + http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + if err := tpl.ExecuteTemplate(w, "", redirURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + +// Returns an http.Handler that serves the vanity URL information for a single +// repository. Each Option gives additional information to agents about the +// repository or provides help to browsers that may have navigated to the vanity +// URL. The WithImport Option is mandatory since the go tool requires it to +// fetch the repository. +func Handler(opts ...Option) http.Handler { + return handlerFrom(compile(opts)) +} + +// Helpers for common VCSs. + +// Redirects gddo to browsable source files for GitHub hosted repositories. +func WithGitHubStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/tree/" + ref + "{/dir}" + file := repoPath + "/blob/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Redirects gddo to browsable source files for Gogs hosted repositories. +func WithGogsStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/src/" + ref + "{/dir}" + file := repoPath + "/src/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Creates a Handler that serves a GitHub repository at a specific importPath. +func GitHubHandler(importPath, user, repo, gitScheme string) http.Handler { + ghImportPath := "github.com/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+ghImportPath), + WithGitHubStyleSource(importPath, "https://"+ghImportPath, "master"), + ) +} + +// Creates a Handler that serves a repository hosted with Gogs at host at a +// specific importPath. +func GogsHandler(importPath, host, user, repo, gitScheme string) http.Handler { + gogsImportPath := host + "/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+gogsImportPath), + WithGogsStyleSource(importPath, "https://"+gogsImportPath, "master"), + ) +} diff --git a/branches/origin/toyohime_test.go b/branches/origin/toyohime_test.go new file mode 100644 index 0000000..bf6598f --- /dev/null +++ b/branches/origin/toyohime_test.go @@ -0,0 +1,91 @@ +package toyohime + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGoTool(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/subpkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + h := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + h.ServeHTTP(res, req) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + + expected := `` + if !strings.Contains(string(body), expected) { + t.Fatalf("Expecting url '%v' body to contain html meta tag: '%v', but got:\n'%v'", test.path, expected, string(body)) + } + + expected = "text/html; charset=utf-8" + if res.HeaderMap.Get("content-type") != expected { + t.Fatalf("Expecting content type '%v', but got '%v'", expected, res.HeaderMap.Get("content-type")) + } + + if res.Code != http.StatusOK { + t.Fatalf("Expected response status 200, but got %v", res.Code) + } + } +} + +func TestBrowserGoDoc(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/sub/foo", "https://godocs.io/go.jonnrb.io/pkg/sub/foo"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + srv := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + srv.ServeHTTP(res, req) + + if res.Code != http.StatusTemporaryRedirect { + t.Fatalf("Expected response status %v, but got %v", http.StatusTemporaryRedirect, res.Code) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + if !strings.Contains(string(body), test.result) { + t.Fatalf("Expecting '%v' be contained in '%v'", test.result, string(body)) + } + } +} + +func ExampleGitHubHandler() { + // Redirects the vanity import path "go.jonnrb.io/vanity" to the code hosted + // on GitHub by user (or organization) "jonnrb" in repo "vanity" using the + // git over "https". + h := GitHubHandler("go.jonnrb.io/vanity", "jonnrb", "vanity", "https") + + http.Handle("/vanity", h) + http.Handle("/vanity/", h) // to handle requests for subpackages. + + http.ListenAndServe(":http", nil) +} diff --git a/branches/origin/version.go b/branches/origin/version.go new file mode 100644 index 0000000..81e847c --- /dev/null +++ b/branches/origin/version.go @@ -0,0 +1,50 @@ +package toyohime + +import ( + "fmt" + "runtime/debug" + "strings" +) + +const ( + defaultVersion = "0.0.0" + defaultCommit = "HEAD" + defaultBuild = "0000-01-01:00:00+00:00" +) + +var ( + // Version is the tagged release version in the form .. + // following semantic versioning and is overwritten by the build system. + Version = defaultVersion + + // Commit is the commit sha of the build (normally from Git) and is overwritten + // by the build system. + Commit = defaultCommit + + // Build is the date and time of the build as an RFC3339 formatted string + // and is overwritten by the build system. + Build = defaultBuild +) + +// FullVersion display the full version and build +func FullVersion() string { + var sb strings.Builder + + isDefault := Version == defaultVersion && Commit == defaultCommit && Build == defaultBuild + + if !isDefault { + sb.WriteString(fmt.Sprintf("%s@%s %s", Version, Commit, Build)) + } + + if info, ok := debug.ReadBuildInfo(); ok { + if isDefault { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Version)) + } + sb.WriteString(fmt.Sprintf(" %s", info.GoVersion)) + if info.Main.Sum != "" { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Sum)) + } + } + + return sb.String() +} diff --git a/trunk/.gitignore b/trunk/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/trunk/LICENSE b/trunk/LICENSE new file mode 100644 index 0000000..d101e81 --- /dev/null +++ b/trunk/LICENSE @@ -0,0 +1,42 @@ +Copyright (c) 2018, Jon Betti +Copyright (c) 2023, Izuru Yakumo + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +Copyright (c) 2016, Kare Nuorteva +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/trunk/Makefile b/trunk/Makefile new file mode 100644 index 0000000..903799b --- /dev/null +++ b/trunk/Makefile @@ -0,0 +1,26 @@ +GO ?= go +GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=$(VERSION) -X `go list`.Commit=$(COMMIT) -X `go list`.Build=$(BUILD)" -tags "static_build" +PREFIX ?= /usr/local + +VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"` +COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"` +BRANCH = `git rev-parse --abbrev-ref HEAD` +BUILD = `git show -s --pretty=format:%cI` + +GOARCH ?= amd64 +GOOS ?= linux + +all: toyohime + +toyohime: + ${GO} build ${GOFLAGS} ./cmd/toyohime +clean: + rm toyohime +install: + install -m0755 toyohime ${DESTDIR}${PREFIX}/bin/yorihime + install -Dm0044 toyohime.1 ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 +test: + ${GO} test . +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/toyohime + rm -f ${DESTDIR}${PREFIX}/share/man/man1/toyohime.1 diff --git a/trunk/README.md b/trunk/README.md new file mode 100644 index 0000000..08a2111 --- /dev/null +++ b/trunk/README.md @@ -0,0 +1,25 @@ +# Toyohime +Fork of [go.jonnrb.io/vanity](https://go.jonnrb.io/vanity) + +A vanity import path is any import path that can be downloaded with +`go get` but isn't otherwise blessed by the `go` tool (e.g. GitHub, +BitBucket, etc.). A commonly used vanity import path is +"golang.org/x/...". This package attempts to mimic the behavior of +"golang.org/x/..." as closely as possible. + +## Features + +* Redirects browsers to godocs.io (or somewhere else) +* Redirects Go tool to VCS +* Redirects pkg.go.dev to browsable files + +## Installation + +```bash +go install marisa.chaotic.ninja/toyohime/cmd/toyohime@latest +``` + +## Specification + - [Remote Import Paths](https://golang.org/cmd/go/#hdr-Remote_import_paths) + - [GDDO Source Code Links](https://github.com/golang/gddo/wiki/Source-Code-Links) + - [Custom Import Path Checking](https://docs.google.com/document/d/1jVFkZTcYbNLaTxXD9OcGfn7vYv5hWtPx9--lTx1gPMs/edit) diff --git a/trunk/cmd/toyohime/README.md b/trunk/cmd/toyohime/README.md new file mode 100644 index 0000000..0bf7f81 --- /dev/null +++ b/trunk/cmd/toyohime/README.md @@ -0,0 +1,21 @@ +# Toyohime (command) + +Runs a barebones vanity server over HTTP. + +## Usage + +``` +./toyohime [-index] fqdn [repo file] +``` + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +If repo file is not given, "./repos" is used. The file has the following format: + +``` +pkgroot vcsScheme://vcsHost/user/repo +pkgroot2 vcsScheme://vcsHost/user/repo2 +``` + +vcsHost is either a [Gogs](https://gogs.io) server (that's what I use) or [GitHub](https://github.com). I'm open to supporting other VCSs but I'm not sure what that would look like. diff --git a/trunk/cmd/toyohime/dynamic_handler.go b/trunk/cmd/toyohime/dynamic_handler.go new file mode 100644 index 0000000..9087230 --- /dev/null +++ b/trunk/cmd/toyohime/dynamic_handler.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + "net/http" + "sync" + + "github.com/fsnotify/fsnotify" +) + +type dynamicHandler struct { + *fsnotify.Watcher + + h http.Handler + unhealthy bool + mu sync.RWMutex +} + +func newDynamicHandler(file string, generator func() (http.Handler, error)) *dynamicHandler { + w, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("Failed to create fsnotify.Watcher: %v", err) + } + + if err := w.Add(file); err != nil { + log.Fatalf("Could not watch file %q: %v", file, err) + } + + h, err := generator() + if err != nil { + log.Fatalf("Failed generating initial handler: %v", err) + } + + dh := &dynamicHandler{Watcher: w, h: h} + updateHandler := func(h http.Handler, err error) error { + dh.mu.Lock() + defer dh.mu.Unlock() + + if err != nil { + dh.unhealthy = true + return err + } + dh.unhealthy = false + + if h == nil { + panic("nil handler returned from generator") + } + dh.h = h + return nil + } + + go func() { + log.Printf("Watching for changes to %q", file) + for { + select { + case evt, ok := <-w.Events: + if !ok { + return + } + if evt.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename) == 0 { + continue + } + if err := updateHandler(generator()); err != nil { + log.Printf("Error switching to new handler: %v", err) + } else { + log.Printf("Updated to new handler based on %q", file) + } + case err, ok := <-w.Errors: + if !ok { + return + } + log.Printf("Error in fsnotify.Watcher: %v", err) + } + } + }() + + return dh +} + +func (dh *dynamicHandler) IsHealthy() bool { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return !dh.unhealthy +} + +func (dh *dynamicHandler) getHandler() http.Handler { + dh.mu.RLock() + defer dh.mu.RUnlock() + + return dh.h +} + +func (dh *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dh.getHandler().ServeHTTP(w, r) +} diff --git a/trunk/cmd/toyohime/main.go b/trunk/cmd/toyohime/main.go new file mode 100644 index 0000000..2204d40 --- /dev/null +++ b/trunk/cmd/toyohime/main.go @@ -0,0 +1,282 @@ +/* +Runs a barebones vanity server over HTTP. + +Usage + + ./toyohime [-index] [-nohealthz] fqdn [repo file] + +The "-index" flag enables an index page at "/" that lists all repos hosted on +this server. + +The "-nohealthz" flag disables the "/healthz" endpoint that returns a 200 OK +when everything is OK. + +The "-watch" flag watches the repo file for changes. When it is updated, the +updated version will be used for serving. + +If repo file is not given, "./repos" is used. The file has the following format: + + pkgroot vcsScheme://vcsHost/user/repo + pkgroot2 vcsScheme://vcsHost/user/repo2 + +vcsHost is either a Gogs server (that's what I use) or github.com. I'm open to +supporting other VCSs but I'm not sure what that would look like. +*/ +package main // go.jonnrb.io/vanity + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "html/template" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "marisa.chaotic.ninja/toyohime" +) + +var ( + showIndex = flag.Bool("index", false, "Show a list of repos at /") + noHealthz = flag.Bool("nohealthz", false, "Disable healthcheck endpoint at /healthz") + watch = flag.Bool("watch", false, "Watch repos file for changes and reload") + listenPort = flag.String("listen", ":8080", "Port to bind on") +) + +var ( + host string // param 1 + reposPath string = "repos" // param 2 +) + +func serveRepo(mux *http.ServeMux, root string, u *url.URL) { + vcsScheme, vcsHost := u.Scheme, u.Host + + // Get ["", "user", "repo"]. + pathParts := strings.Split(u.Path, "/") + if len(pathParts) != 3 { + log.Fatalf("Repo URL must be of the form vcsScheme://vcsHost/user/repo but got %q", u.String()) + } + user, repo := pathParts[1], pathParts[2] + + importPath := host + "/" + root + var h http.Handler + if vcsHost == "github.com" { + h = toyohime.GitHubHandler(importPath, user, repo, vcsScheme) + } else { + h = toyohime.GogsHandler(importPath, vcsHost, user, repo, vcsScheme) + } + mux.Handle("/"+root, h) + mux.Handle("/"+root+"/", h) +} + +func addRepoHandlers(mux *http.ServeMux, r io.Reader) error { + indexMap := map[string]string{} + + sc := bufio.NewScanner(r) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + switch len(fields) { + case 0: + continue + case 2: + // Pass + default: + return fmt.Errorf("expected line of form \"path vcsScheme://vcsHost/user/repo\" but got %q", sc.Text()) + } + + if *showIndex { + indexMap[fields[0]] = fields[1] + } + + path := fields[0] + u, err := url.Parse(fields[1]) + if err != nil { + return fmt.Errorf("repo was not a valid URL: %q", fields[1]) + } + + serveRepo(mux, path, u) + } + + if !*showIndex { + return nil + } + + var b bytes.Buffer + err := template.Must(template.New("").Parse(` + + + +Import paths hosted at {{ .Host }} + + + +{{ $host := .Host }} +
+

でホストされているインポート パス {{ html $host }}

+
+
+ +{{ range $root, $repo := .IndexMap }} + +
+
+* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +
+
+
+

{{ html $root }}

+Package
+Repository +{{ else }} +Nothing here. +{{ end }} +
+
+

~から明らかに盗まれた azukifont.com

+
+ + +`)).Execute(&b, struct { + IndexMap map[string]string + Host string + }{ + IndexMap: indexMap, + Host: host, + }) + if err != nil { + return fmt.Errorf("couldn't create index page: %v", err) + } + buf := b.Bytes() + + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + io.Copy(w, bytes.NewReader(buf)) + })) + return nil +} + +func registerHealthz(mux *http.ServeMux, isHealthy func() bool) { + mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isHealthy() { + io.WriteString(w, "OK\r\n") + } else { + http.Error(w, "internal error\r\n", http.StatusInternalServerError) + } + })) +} + +var healthcheck = func() bool { + return true +} + +func generateHandler() (http.Handler, error) { + mux := http.NewServeMux() + + f, err := os.Open(reposPath) + if err != nil { + return nil, fmt.Errorf("error opening %q: %v", reposPath, err) + } + if err := addRepoHandlers(mux, f); err != nil { + return nil, err + } + + if !*noHealthz { + registerHealthz(mux, healthcheck) + } + return mux, nil +} + +func buildServer(h http.Handler) *http.Server { + return &http.Server{ + // This should be sufficient. + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 5 * time.Second, + + Addr: *listenPort, + Handler: h, + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: %s fqdn [repos file]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + host = flag.Arg(0) + if host == "" { + flag.Usage() + os.Exit(-1) + } + + if override := flag.Arg(1); override != "" { + reposPath = override + } + + var h http.Handler + if *watch { + dh := newDynamicHandler(reposPath, generateHandler) + healthcheck = dh.IsHealthy + defer dh.Close() + h = dh + } else { + var err error + h, err = generateHandler() + if err != nil { + log.Printf("Error generating handler: %v", err) + } + } + + srv := buildServer(h) + + log.Printf("starting toyohime %v on port %v\n", toyohime.FullVersion(), *listenPort) + log.Println(srv.ListenAndServe()) +} diff --git a/trunk/go.mod b/trunk/go.mod new file mode 100644 index 0000000..e2712e7 --- /dev/null +++ b/trunk/go.mod @@ -0,0 +1,5 @@ +module marisa.chaotic.ninja/toyohime + +go 1.14 + +require github.com/fsnotify/fsnotify v1.4.9 diff --git a/trunk/go.sum b/trunk/go.sum new file mode 100644 index 0000000..b12c1ed --- /dev/null +++ b/trunk/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/trunk/rc.d/freebsd.sh b/trunk/rc.d/freebsd.sh new file mode 100644 index 0000000..005a6e6 --- /dev/null +++ b/trunk/rc.d/freebsd.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON NETWORKING +# BEFORE: LOGIN +# KEYWORD: shutdown + +. /etc/rc.subr + +name="toyohime" +rcvar="${name}_enable" + +: ${toyohime_enable="NO"} +: ${toyohime_fqdn="localhost"} +: ${toyohime_user="www"} +: ${toyohime_group="www"} +: ${toyohime_repos="/usr/local/etc/toyohime-repos"} +: ${toyohime_address="127.0.0.1:8080"} + +command="/usr/sbin/daemon" +pidfile="/var/run/${name}.pid" +command_args="-p ${pidfile} -u ${toyohime_user} /usr/local/bin/${name} -listen ${toyohime_address} -index -watch ${toyohime_fqdn} ${toyohime_repos}" + +load_rc_config "${name}" +run_rc_command "$1" diff --git a/trunk/rc.d/immortal.yml b/trunk/rc.d/immortal.yml new file mode 100644 index 0000000..7e0008b --- /dev/null +++ b/trunk/rc.d/immortal.yml @@ -0,0 +1,2 @@ +cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos +user: www diff --git a/trunk/rc.d/netbsd.sh b/trunk/rc.d/netbsd.sh new file mode 100644 index 0000000..8c9cdd6 --- /dev/null +++ b/trunk/rc.d/netbsd.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# $TheSupernovaDuo$ +# +# PROVIDE: toyohime +# REQUIRE: DAEMON +# BEFORE: LOGIN +# KEYWORD: shutdown + +$rc_subr_loaded . /etc/rc.subr + +name="toyohime" +rcvar="$name" +command="/usr/local/bin/$name" +command_args="-listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos" + +load_rc_config "$name" +run_rc_command "$1" diff --git a/trunk/rc.d/openbsd.ksh b/trunk/rc.d/openbsd.ksh new file mode 100644 index 0000000..5c56be0 --- /dev/null +++ b/trunk/rc.d/openbsd.ksh @@ -0,0 +1,11 @@ +#!/bin/ksh +# $TheSupernovaDuo$ + +. /etc/rc.d/rc.subr + +daemon="/usr/local/bin/toyohime" +daemon_flags="-listen 127.0.0.1:8080 -index localhost /etc/toyohime-repos" +daemon_user="www" +rc_bg=YES + +rc_cmd "$1" diff --git a/trunk/toyohime.1 b/trunk/toyohime.1 new file mode 100644 index 0000000..7ba383f --- /dev/null +++ b/trunk/toyohime.1 @@ -0,0 +1,25 @@ +.Dd $Mdocdate$ +.Dt TOYOHIME 1 +.Os +.Sh NAME +.Nm toyohime +.Nd Library and CLI for hosting custom vanity URIs for the Go tool +.Sh SYNOPSIS +.Nm +.Op Fl index +.Op Fl listen Ar ip:port +.Op Fl nohealthz +.Op Fl watch +.Op Ar fqdn +.Op Ar path/to/repos/file +.Sh DESCRIPTION +.Nm +is a library and command line implementation +that allows developers to have their own path +for their Go packages, closely replicating +the behavior of golang.org/x/.. as much as +possible. +.Sh AUTHORS +.An Jon Betti Aq Mt jonbetti@gmail.com +.Sh MAINTAINERS +.An Izuru Yakumo Aq Mt yakumo.izuru@chaotic.ninja diff --git a/trunk/toyohime.go b/trunk/toyohime.go new file mode 100644 index 0000000..0439d4d --- /dev/null +++ b/trunk/toyohime.go @@ -0,0 +1,179 @@ +/* +Package toyohime implements custom import paths (Go vanity URLs) as an HTTP +handler that can be installed at the vanity URL. +*/ +package toyohime // import "go.jonnrb.io/vanity" <- original one for reference + +import ( + "fmt" + "html/template" + "net/http" + "strings" +) + +type config struct { + importTag *string + sourceTag *string + redir Redirector +} + +// Configures the Handler. The only required option is WithImport. +type Option func(*config) + +// Instructs the go tool where to fetch the repo at vcsRoot and the importPath +// that tree should be rooted at. +func WithImport(importPath, vcs, vcsRoot string) Option { + importTag := "" + return func(cfg *config) { + if cfg.importTag != nil { + panic(fmt.Sprintf("vanity: existing import tag: %s", *cfg.importTag)) + } + cfg.importTag = &importTag + } +} + +// Instructs gddo (godoc.org) how to direct browsers to browsable source code +// for packages and their contents rooted at prefix. +// +// home specifies the home page of prefix, directory gives a format for how to +// browse a directory, and file gives a format for how to view a file and go to +// specific lines within it. +// +// More information can be found at https://github.com/golang/gddo/wiki/Source-Code-Links. +// +func WithSource(prefix, home, directory, file string) Option { + sourceTag := "" + return func(cfg *config) { + if cfg.sourceTag != nil { + panic(fmt.Sprintf("vanity: existing source tag: %s", *cfg.importTag)) + } + cfg.sourceTag = &sourceTag + } +} + +// When a browser navigates to the vanity URL of pkg, this function rewrites +// pkg to a browsable URL. +type Redirector func(pkg string) (url string) + +func WithRedirector(redir Redirector) Option { + return func(cfg *config) { + if cfg.redir != nil { + panic("vanity: existing Redirector") + } + cfg.redir = redir + } +} + +func compile(opts []Option) (*template.Template, Redirector) { + // Process options. + var cfg config + for _, opt := range opts { + opt(&cfg) + } + + // A WithImport is required. + if cfg.importTag == nil { + panic("vanity: WithImport is required") + } + + tags := []string{*cfg.importTag} + if cfg.sourceTag != nil { + tags = append(tags, *cfg.sourceTag) + } + tagBlk := strings.Join(tags, "\n") + + h := fmt.Sprintf(` + + + +%s + + + +Nothing to see here; move along. + + +`, tagBlk) + + // Use default GDDO Redirector. + if cfg.redir == nil { + cfg.redir = func(pkg string) string { + return "https://pkg.go.dev/" + pkg + } + } + + return template.Must(template.New("").Parse(h)), cfg.redir +} + +func handlerFrom(tpl *template.Template, redir Redirector) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only method supported is GET. + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(w, http.StatusText(status), status) + return + } + + pkg := r.Host + r.URL.Path + redirURL := redir(pkg) + + // Issue an HTTP redirect if this is definitely a browser. + if r.FormValue("go-get") != "1" { + http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + if err := tpl.ExecuteTemplate(w, "", redirURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + +// Returns an http.Handler that serves the vanity URL information for a single +// repository. Each Option gives additional information to agents about the +// repository or provides help to browsers that may have navigated to the vanity +// URL. The WithImport Option is mandatory since the go tool requires it to +// fetch the repository. +func Handler(opts ...Option) http.Handler { + return handlerFrom(compile(opts)) +} + +// Helpers for common VCSs. + +// Redirects gddo to browsable source files for GitHub hosted repositories. +func WithGitHubStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/tree/" + ref + "{/dir}" + file := repoPath + "/blob/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Redirects gddo to browsable source files for Gogs hosted repositories. +func WithGogsStyleSource(importPath, repoPath, ref string) Option { + directory := repoPath + "/src/" + ref + "{/dir}" + file := repoPath + "/src/" + ref + "{/dir}/{file}#L{line}" + + return WithSource(importPath, repoPath, directory, file) +} + +// Creates a Handler that serves a GitHub repository at a specific importPath. +func GitHubHandler(importPath, user, repo, gitScheme string) http.Handler { + ghImportPath := "github.com/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+ghImportPath), + WithGitHubStyleSource(importPath, "https://"+ghImportPath, "master"), + ) +} + +// Creates a Handler that serves a repository hosted with Gogs at host at a +// specific importPath. +func GogsHandler(importPath, host, user, repo, gitScheme string) http.Handler { + gogsImportPath := host + "/" + user + "/" + repo + return Handler( + WithImport(importPath, "git", gitScheme+"://"+gogsImportPath), + WithGogsStyleSource(importPath, "https://"+gogsImportPath, "master"), + ) +} diff --git a/trunk/toyohime_test.go b/trunk/toyohime_test.go new file mode 100644 index 0000000..bf6598f --- /dev/null +++ b/trunk/toyohime_test.go @@ -0,0 +1,91 @@ +package toyohime + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGoTool(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + {"/pkg/subpkg?go-get=1", "go.jonnrb.io/pkg git https://github.com/jonnrb/pkg"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + h := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + h.ServeHTTP(res, req) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + + expected := `` + if !strings.Contains(string(body), expected) { + t.Fatalf("Expecting url '%v' body to contain html meta tag: '%v', but got:\n'%v'", test.path, expected, string(body)) + } + + expected = "text/html; charset=utf-8" + if res.HeaderMap.Get("content-type") != expected { + t.Fatalf("Expecting content type '%v', but got '%v'", expected, res.HeaderMap.Get("content-type")) + } + + if res.Code != http.StatusOK { + t.Fatalf("Expected response status 200, but got %v", res.Code) + } + } +} + +func TestBrowserGoDoc(t *testing.T) { + tests := []struct { + path string + result string + }{ + {"/pkg", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/", "https://godocs.io/go.jonnrb.io/pkg"}, + {"/pkg/sub/foo", "https://godocs.io/go.jonnrb.io/pkg/sub/foo"}, + } + for _, test := range tests { + res := httptest.NewRecorder() + req, err := http.NewRequest("GET", "go.jonnrb.io"+test.path, nil) + if err != nil { + t.Fatal(err) + } + srv := GitHubHandler("go.jonnrb.io/pkg", "jonnrb", "pkg", "https") + srv.ServeHTTP(res, req) + + if res.Code != http.StatusTemporaryRedirect { + t.Fatalf("Expected response status %v, but got %v", http.StatusTemporaryRedirect, res.Code) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("reading response body failed with error: %v", err) + } + if !strings.Contains(string(body), test.result) { + t.Fatalf("Expecting '%v' be contained in '%v'", test.result, string(body)) + } + } +} + +func ExampleGitHubHandler() { + // Redirects the vanity import path "go.jonnrb.io/vanity" to the code hosted + // on GitHub by user (or organization) "jonnrb" in repo "vanity" using the + // git over "https". + h := GitHubHandler("go.jonnrb.io/vanity", "jonnrb", "vanity", "https") + + http.Handle("/vanity", h) + http.Handle("/vanity/", h) // to handle requests for subpackages. + + http.ListenAndServe(":http", nil) +} diff --git a/trunk/version.go b/trunk/version.go new file mode 100644 index 0000000..81e847c --- /dev/null +++ b/trunk/version.go @@ -0,0 +1,50 @@ +package toyohime + +import ( + "fmt" + "runtime/debug" + "strings" +) + +const ( + defaultVersion = "0.0.0" + defaultCommit = "HEAD" + defaultBuild = "0000-01-01:00:00+00:00" +) + +var ( + // Version is the tagged release version in the form .. + // following semantic versioning and is overwritten by the build system. + Version = defaultVersion + + // Commit is the commit sha of the build (normally from Git) and is overwritten + // by the build system. + Commit = defaultCommit + + // Build is the date and time of the build as an RFC3339 formatted string + // and is overwritten by the build system. + Build = defaultBuild +) + +// FullVersion display the full version and build +func FullVersion() string { + var sb strings.Builder + + isDefault := Version == defaultVersion && Commit == defaultCommit && Build == defaultBuild + + if !isDefault { + sb.WriteString(fmt.Sprintf("%s@%s %s", Version, Commit, Build)) + } + + if info, ok := debug.ReadBuildInfo(); ok { + if isDefault { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Version)) + } + sb.WriteString(fmt.Sprintf(" %s", info.GoVersion)) + if info.Main.Sum != "" { + sb.WriteString(fmt.Sprintf(" %s", info.Main.Sum)) + } + } + + return sb.String() +}