--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+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)
+}
--- /dev/null
+/*
+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(`
+<!DOCTYPE html>
+<html>
+<head>
+<title>Import paths hosted at {{ .Host }}</title>
+<style type="text/css">
+body,td{font-size:11pt; color:#4d4d4d; font-family:ms pgothic, ms gothic, osaka; background-color:#f8dfdf}
+a:link{color:#cc9999; text-decoration:none}
+a:visited{color:#cc9999; text-decoration:none}
+a:active{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+a:hover{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+.yohaku{margin-top:30px}
+.dia{line-height:130%}
+.star1{font-size:22px; color:#cc9999}
+.position1{position:relative; top:7px}
+.line{border-top:2px dotted #cc9999}
+.waku{border:1px solid #cc9999}
+.bg{background-color:#ffffff}
+</style>
+</head>
+<body>
+{{ $host := .Host }}
+<div align="center">
+<p>でホストされているインポート パス {{ html $host }}</p>
+</div>
+<div align="center">
+<table width="470" class="bg waku yohaku" cellspacing="0" style="margin-bottom: 20px">
+{{ range $root, $repo := .IndexMap }}
+<tr><td class="bg" style="padding-top: 5px; padding-bottom: 5px">
+<div class="star1" align="center">
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+</div>
+</td></tr>
+<tr><td class="bg line dia" style="padding: 15px 10px 10px 10px">
+<center>
+<h2>{{ html $root }}</h2>
+<a href="https://{{ html $host }}/{{ html $root }}">Package</a> <br/>
+<a href="{{ html $repo }}">Repository</a></li>
+{{ else }}
+Nothing here.
+{{ end }}
+</center>
+</table>
+<p>~から明らかに盗まれた <a href="http://azukifont.com">azukifont.com</a></p>
+</div>
+</body>
+</html>
+`)).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())
+}
--- /dev/null
+module marisa.chaotic.ninja/toyohime
+
+go 1.14
+
+require github.com/fsnotify/fsnotify v1.4.9
--- /dev/null
+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=
--- /dev/null
+#!/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"
--- /dev/null
+cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos
+user: www
--- /dev/null
+#!/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"
--- /dev/null
+#!/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"
--- /dev/null
+.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
--- /dev/null
+/*
+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 := "<meta name=\"go-import\" content=\"" + importPath + " " +
+ vcs + " " + vcsRoot + "\">"
+ 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 := "<meta name=\"go-source\" content=\"" + prefix + " " +
+ home + " " + directory + " " + file + "\">"
+ 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(`<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+%s
+<meta http-equiv="refresh" content="0; url={{ . }}">
+</head>
+<body>
+Nothing to see here; <a href="{{ . }}">move along</a>.
+</body>
+</html>
+`, 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"),
+ )
+}
--- /dev/null
+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 := `<meta name="go-import" content="` + test.result + `">`
+ 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)
+}
--- /dev/null
+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 <major>.<minor>.<patch>
+ // 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()
+}
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+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)
+}
--- /dev/null
+/*
+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(`
+<!DOCTYPE html>
+<html>
+<head>
+<title>Import paths hosted at {{ .Host }}</title>
+<style type="text/css">
+body,td{font-size:11pt; color:#4d4d4d; font-family:ms pgothic, ms gothic, osaka; background-color:#f8dfdf}
+a:link{color:#cc9999; text-decoration:none}
+a:visited{color:#cc9999; text-decoration:none}
+a:active{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+a:hover{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+.yohaku{margin-top:30px}
+.dia{line-height:130%}
+.star1{font-size:22px; color:#cc9999}
+.position1{position:relative; top:7px}
+.line{border-top:2px dotted #cc9999}
+.waku{border:1px solid #cc9999}
+.bg{background-color:#ffffff}
+</style>
+</head>
+<body>
+{{ $host := .Host }}
+<div align="center">
+<p>でホストされているインポート パス {{ html $host }}</p>
+</div>
+<div align="center">
+<table width="470" class="bg waku yohaku" cellspacing="0" style="margin-bottom: 20px">
+{{ range $root, $repo := .IndexMap }}
+<tr><td class="bg" style="padding-top: 5px; padding-bottom: 5px">
+<div class="star1" align="center">
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+</div>
+</td></tr>
+<tr><td class="bg line dia" style="padding: 15px 10px 10px 10px">
+<center>
+<h2>{{ html $root }}</h2>
+<a href="https://{{ html $host }}/{{ html $root }}">Package</a> <br/>
+<a href="{{ html $repo }}">Repository</a></li>
+{{ else }}
+Nothing here.
+{{ end }}
+</center>
+</table>
+<p>~から明らかに盗まれた <a href="http://azukifont.com">azukifont.com</a></p>
+</div>
+</body>
+</html>
+`)).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())
+}
--- /dev/null
+module marisa.chaotic.ninja/toyohime
+
+go 1.14
+
+require github.com/fsnotify/fsnotify v1.4.9
--- /dev/null
+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=
--- /dev/null
+#!/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"
--- /dev/null
+cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos
+user: www
--- /dev/null
+#!/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"
--- /dev/null
+#!/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"
--- /dev/null
+.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
--- /dev/null
+/*
+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 := "<meta name=\"go-import\" content=\"" + importPath + " " +
+ vcs + " " + vcsRoot + "\">"
+ 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 := "<meta name=\"go-source\" content=\"" + prefix + " " +
+ home + " " + directory + " " + file + "\">"
+ 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(`<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+%s
+<meta http-equiv="refresh" content="0; url={{ . }}">
+</head>
+<body>
+Nothing to see here; <a href="{{ . }}">move along</a>.
+</body>
+</html>
+`, 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"),
+ )
+}
--- /dev/null
+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 := `<meta name="go-import" content="` + test.result + `">`
+ 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)
+}
--- /dev/null
+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 <major>.<minor>.<patch>
+ // 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()
+}
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+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)
+}
--- /dev/null
+/*
+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(`
+<!DOCTYPE html>
+<html>
+<head>
+<title>Import paths hosted at {{ .Host }}</title>
+<style type="text/css">
+body,td{font-size:11pt; color:#4d4d4d; font-family:ms pgothic, ms gothic, osaka; background-color:#f8dfdf}
+a:link{color:#cc9999; text-decoration:none}
+a:visited{color:#cc9999; text-decoration:none}
+a:active{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+a:hover{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+.yohaku{margin-top:30px}
+.dia{line-height:130%}
+.star1{font-size:22px; color:#cc9999}
+.position1{position:relative; top:7px}
+.line{border-top:2px dotted #cc9999}
+.waku{border:1px solid #cc9999}
+.bg{background-color:#ffffff}
+</style>
+</head>
+<body>
+{{ $host := .Host }}
+<div align="center">
+<p>でホストされているインポート パス {{ html $host }}</p>
+</div>
+<div align="center">
+<table width="470" class="bg waku yohaku" cellspacing="0" style="margin-bottom: 20px">
+{{ range $root, $repo := .IndexMap }}
+<tr><td class="bg" style="padding-top: 5px; padding-bottom: 5px">
+<div class="star1" align="center">
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+</div>
+</td></tr>
+<tr><td class="bg line dia" style="padding: 15px 10px 10px 10px">
+<center>
+<h2>{{ html $root }}</h2>
+<a href="https://{{ html $host }}/{{ html $root }}">Package</a> <br/>
+<a href="{{ html $repo }}">Repository</a></li>
+{{ else }}
+Nothing here.
+{{ end }}
+</center>
+</table>
+<p>~から明らかに盗まれた <a href="http://azukifont.com">azukifont.com</a></p>
+</div>
+</body>
+</html>
+`)).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())
+}
--- /dev/null
+module marisa.chaotic.ninja/toyohime
+
+go 1.14
+
+require github.com/fsnotify/fsnotify v1.4.9
--- /dev/null
+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=
--- /dev/null
+#!/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"
--- /dev/null
+cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos
+user: www
--- /dev/null
+#!/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"
--- /dev/null
+#!/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"
--- /dev/null
+.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
--- /dev/null
+/*
+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 := "<meta name=\"go-import\" content=\"" + importPath + " " +
+ vcs + " " + vcsRoot + "\">"
+ 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 := "<meta name=\"go-source\" content=\"" + prefix + " " +
+ home + " " + directory + " " + file + "\">"
+ 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(`<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+%s
+<meta http-equiv="refresh" content="0; url={{ . }}">
+</head>
+<body>
+Nothing to see here; <a href="{{ . }}">move along</a>.
+</body>
+</html>
+`, 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"),
+ )
+}
--- /dev/null
+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 := `<meta name="go-import" content="` + test.result + `">`
+ 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)
+}
--- /dev/null
+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 <major>.<minor>.<patch>
+ // 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()
+}
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+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)
+}
--- /dev/null
+/*
+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(`
+<!DOCTYPE html>
+<html>
+<head>
+<title>Import paths hosted at {{ .Host }}</title>
+<style type="text/css">
+body,td{font-size:11pt; color:#4d4d4d; font-family:ms pgothic, ms gothic, osaka; background-color:#f8dfdf}
+a:link{color:#cc9999; text-decoration:none}
+a:visited{color:#cc9999; text-decoration:none}
+a:active{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+a:hover{color:#cc9999; text-decoration:none; position:relative; top:3px; left:3px}
+.yohaku{margin-top:30px}
+.dia{line-height:130%}
+.star1{font-size:22px; color:#cc9999}
+.position1{position:relative; top:7px}
+.line{border-top:2px dotted #cc9999}
+.waku{border:1px solid #cc9999}
+.bg{background-color:#ffffff}
+</style>
+</head>
+<body>
+{{ $host := .Host }}
+<div align="center">
+<p>でホストされているインポート パス {{ html $host }}</p>
+</div>
+<div align="center">
+<table width="470" class="bg waku yohaku" cellspacing="0" style="margin-bottom: 20px">
+{{ range $root, $repo := .IndexMap }}
+<tr><td class="bg" style="padding-top: 5px; padding-bottom: 5px">
+<div class="star1" align="center">
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+<span style="position:relative; top:5px">*</span>
+<span style="position:relative; top:-5px">*</span>
+</div>
+</td></tr>
+<tr><td class="bg line dia" style="padding: 15px 10px 10px 10px">
+<center>
+<h2>{{ html $root }}</h2>
+<a href="https://{{ html $host }}/{{ html $root }}">Package</a> <br/>
+<a href="{{ html $repo }}">Repository</a></li>
+{{ else }}
+Nothing here.
+{{ end }}
+</center>
+</table>
+<p>~から明らかに盗まれた <a href="http://azukifont.com">azukifont.com</a></p>
+</div>
+</body>
+</html>
+`)).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())
+}
--- /dev/null
+module marisa.chaotic.ninja/toyohime
+
+go 1.14
+
+require github.com/fsnotify/fsnotify v1.4.9
--- /dev/null
+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=
--- /dev/null
+#!/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"
--- /dev/null
+cmd: /usr/local/bin/toyohime -listen 127.0.0.1:8080 -index go.example.com /usr/local/etc/toyohime-repos
+user: www
--- /dev/null
+#!/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"
--- /dev/null
+#!/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"
--- /dev/null
+.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
--- /dev/null
+/*
+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 := "<meta name=\"go-import\" content=\"" + importPath + " " +
+ vcs + " " + vcsRoot + "\">"
+ 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 := "<meta name=\"go-source\" content=\"" + prefix + " " +
+ home + " " + directory + " " + file + "\">"
+ 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(`<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+%s
+<meta http-equiv="refresh" content="0; url={{ . }}">
+</head>
+<body>
+Nothing to see here; <a href="{{ . }}">move along</a>.
+</body>
+</html>
+`, 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"),
+ )
+}
--- /dev/null
+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 := `<meta name="go-import" content="` + test.result + `">`
+ 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)
+}
--- /dev/null
+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 <major>.<minor>.<patch>
+ // 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()
+}