]> Git repositories of Izuru Yakumo - toyohime.git/commitdiff
Mirrored from toyohime.git master
authorwww <www@54e66ccc-7408-294c-82d8-82ecf80158e7>
Sun, 29 Sep 2024 21:30:07 +0000 (21:30 +0000)
committerwww <www@54e66ccc-7408-294c-82d8-82ecf80158e7>
Sun, 29 Sep 2024 21:30:07 +0000 (21:30 +0000)
git-svn-id: https://svn.chaotic.ninja/svn/toyohime-yakumo.izuru@1 54e66ccc-7408-294c-82d8-82ecf80158e7

68 files changed:
branches/master/.gitignore [new file with mode: 0644]
branches/master/LICENSE [new file with mode: 0644]
branches/master/Makefile [new file with mode: 0644]
branches/master/README.md [new file with mode: 0644]
branches/master/cmd/toyohime/README.md [new file with mode: 0644]
branches/master/cmd/toyohime/dynamic_handler.go [new file with mode: 0644]
branches/master/cmd/toyohime/main.go [new file with mode: 0644]
branches/master/go.mod [new file with mode: 0644]
branches/master/go.sum [new file with mode: 0644]
branches/master/rc.d/freebsd.sh [new file with mode: 0644]
branches/master/rc.d/immortal.yml [new file with mode: 0644]
branches/master/rc.d/netbsd.sh [new file with mode: 0644]
branches/master/rc.d/openbsd.ksh [new file with mode: 0644]
branches/master/toyohime.1 [new file with mode: 0644]
branches/master/toyohime.go [new file with mode: 0644]
branches/master/toyohime_test.go [new file with mode: 0644]
branches/master/version.go [new file with mode: 0644]
branches/origin-master/.gitignore [new file with mode: 0644]
branches/origin-master/LICENSE [new file with mode: 0644]
branches/origin-master/Makefile [new file with mode: 0644]
branches/origin-master/README.md [new file with mode: 0644]
branches/origin-master/cmd/toyohime/README.md [new file with mode: 0644]
branches/origin-master/cmd/toyohime/dynamic_handler.go [new file with mode: 0644]
branches/origin-master/cmd/toyohime/main.go [new file with mode: 0644]
branches/origin-master/go.mod [new file with mode: 0644]
branches/origin-master/go.sum [new file with mode: 0644]
branches/origin-master/rc.d/freebsd.sh [new file with mode: 0644]
branches/origin-master/rc.d/immortal.yml [new file with mode: 0644]
branches/origin-master/rc.d/netbsd.sh [new file with mode: 0644]
branches/origin-master/rc.d/openbsd.ksh [new file with mode: 0644]
branches/origin-master/toyohime.1 [new file with mode: 0644]
branches/origin-master/toyohime.go [new file with mode: 0644]
branches/origin-master/toyohime_test.go [new file with mode: 0644]
branches/origin-master/version.go [new file with mode: 0644]
branches/origin/.gitignore [new file with mode: 0644]
branches/origin/LICENSE [new file with mode: 0644]
branches/origin/Makefile [new file with mode: 0644]
branches/origin/README.md [new file with mode: 0644]
branches/origin/cmd/toyohime/README.md [new file with mode: 0644]
branches/origin/cmd/toyohime/dynamic_handler.go [new file with mode: 0644]
branches/origin/cmd/toyohime/main.go [new file with mode: 0644]
branches/origin/go.mod [new file with mode: 0644]
branches/origin/go.sum [new file with mode: 0644]
branches/origin/rc.d/freebsd.sh [new file with mode: 0644]
branches/origin/rc.d/immortal.yml [new file with mode: 0644]
branches/origin/rc.d/netbsd.sh [new file with mode: 0644]
branches/origin/rc.d/openbsd.ksh [new file with mode: 0644]
branches/origin/toyohime.1 [new file with mode: 0644]
branches/origin/toyohime.go [new file with mode: 0644]
branches/origin/toyohime_test.go [new file with mode: 0644]
branches/origin/version.go [new file with mode: 0644]
trunk/.gitignore [new file with mode: 0644]
trunk/LICENSE [new file with mode: 0644]
trunk/Makefile [new file with mode: 0644]
trunk/README.md [new file with mode: 0644]
trunk/cmd/toyohime/README.md [new file with mode: 0644]
trunk/cmd/toyohime/dynamic_handler.go [new file with mode: 0644]
trunk/cmd/toyohime/main.go [new file with mode: 0644]
trunk/go.mod [new file with mode: 0644]
trunk/go.sum [new file with mode: 0644]
trunk/rc.d/freebsd.sh [new file with mode: 0644]
trunk/rc.d/immortal.yml [new file with mode: 0644]
trunk/rc.d/netbsd.sh [new file with mode: 0644]
trunk/rc.d/openbsd.ksh [new file with mode: 0644]
trunk/toyohime.1 [new file with mode: 0644]
trunk/toyohime.go [new file with mode: 0644]
trunk/toyohime_test.go [new file with mode: 0644]
trunk/version.go [new file with mode: 0644]

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