+/*
+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())
+}