| 1 | package server |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "log" |
| 6 | "net/http" |
| 7 | ) |
| 8 | |
| 9 | var landingTmpl = template.Must(template.New("landing").Parse(`<!DOCTYPE html> |
| 10 | <html lang="en"> |
| 11 | <head> |
| 12 | <meta charset="utf-8"> |
| 13 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 14 | <title>{{.Host}} — Go Module Server</title> |
| 15 | <style> |
| 16 | :root{--bg:#fff;--bg2:#f8f9fa;--text:#202124;--text2:#5f6368;--border:#dadce0;--accent:#1a73e8;--green:#188038;--max:900px} |
| 17 | @media(prefers-color-scheme:dark){:root{--bg:#1e1e1e;--bg2:#252526;--text:#d4d4d4;--text2:#a0a0a0;--border:#404040;--accent:#6cb6ff;--green:#81c995}} |
| 18 | *{box-sizing:border-box;margin:0;padding:0} |
| 19 | body{font-family:system-ui,sans-serif;font-size:15px;line-height:1.6;color:var(--text);background:var(--bg)} |
| 20 | a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline} |
| 21 | .container{max-width:var(--max);margin:0 auto;padding:24px} |
| 22 | h1{font-size:28px;margin-bottom:4px} |
| 23 | .subtitle{color:var(--text2);font-size:16px;margin-bottom:32px} |
| 24 | h2{font-size:20px;margin:32px 0 12px} |
| 25 | pre{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:16px;font-family:monospace;font-size:13px;overflow-x:auto;margin-bottom:16px;line-height:1.5} |
| 26 | code{font-family:monospace;font-size:13px;background:var(--bg2);padding:2px 6px;border-radius:3px} |
| 27 | table{width:100%;border-collapse:collapse;margin-bottom:24px} |
| 28 | th{text-align:left;padding:8px 12px;border-bottom:2px solid var(--border);font-size:13px;color:var(--text2);text-transform:uppercase;letter-spacing:0.5px} |
| 29 | td{padding:8px 12px;border-bottom:1px solid var(--border);font-size:14px} |
| 30 | td:first-child{font-family:monospace;font-size:13px} |
| 31 | .note{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px 16px;font-size:14px;color:var(--text2);margin-top:16px} |
| 32 | footer{text-align:center;padding:32px 0 16px;font-size:12px;color:var(--text2)} |
| 33 | </style> |
| 34 | </head> |
| 35 | <body> |
| 36 | <div class="container"> |
| 37 | <h1>{{.Host}}</h1> |
| 38 | <p class="subtitle">Vanity Go module server</p> |
| 39 |
|
| 40 | <h2>Setup</h2> |
| 41 | <p>Configure your Go toolchain to use this proxy:</p> |
| 42 | <pre>export GOPROXY=https://{{.Host}},direct{{if not .SumdbEnabled}} |
| 43 | export GONOSUMDB={{.Host}}{{end}}</pre> |
| 44 |
|
| 45 | {{if .SumdbEnabled}} |
| 46 | <p>This server provides a <a href="/sumdb/{{.Host}}/latest">transparency log</a> (sumdb) for module checksums.</p> |
| 47 | {{end}} |
| 48 |
|
| 49 | <p>Then import modules as usual:</p> |
| 50 | <pre>import "{{.Host}}/<module-name>"</pre> |
| 51 |
|
| 52 | {{if .Modules}} |
| 53 | <h2>Public Modules</h2> |
| 54 | <table> |
| 55 | <thead> |
| 56 | <tr><th>Module</th><th>Repository</th></tr> |
| 57 | </thead> |
| 58 | <tbody> |
| 59 | {{range .Modules}} |
| 60 | <tr> |
| 61 | <td><a href="/{{.Name}}">{{$.Host}}/{{.Name}}</a></td> |
| 62 | <td>{{if .Web}}<a href="{{.Web}}">{{.Web}}</a>{{else}}{{.Repo}}{{end}}</td> |
| 63 | </tr> |
| 64 | {{end}} |
| 65 | </tbody> |
| 66 | </table> |
| 67 | {{end}} |
| 68 |
|
| 69 | {{if .Patterns}} |
| 70 | <h2>Module Patterns</h2> |
| 71 | <p style="margin-bottom:12px;color:var(--text2);font-size:14px">Modules matching these patterns are served automatically.</p> |
| 72 | <table> |
| 73 | <thead> |
| 74 | <tr><th>Pattern</th><th>Repository Template</th></tr> |
| 75 | </thead> |
| 76 | <tbody> |
| 77 | {{range .Patterns}} |
| 78 | <tr> |
| 79 | <td><code>{{.Pattern}}</code></td> |
| 80 | <td>{{.Repo}}</td> |
| 81 | </tr> |
| 82 | {{end}} |
| 83 | </tbody> |
| 84 | </table> |
| 85 | {{end}} |
| 86 |
|
| 87 | {{if .HasAuth}} |
| 88 | <div class="note">Some modules are private and require authentication. Use a <code>Bearer</code> token or HTTP basic auth to access them.</div> |
| 89 | {{end}} |
| 90 | </div> |
| 91 | <footer>Powered by Curator</footer> |
| 92 | </body> |
| 93 | </html> |
| 94 | `)) |
| 95 | |
| 96 | func (s *Server) serveLanding(w http.ResponseWriter, r *http.Request) { |
| 97 | data := map[string]any{ |
| 98 | "Host": s.Cfg.Host, |
| 99 | "SumdbEnabled": s.Cfg.Sumdb != nil && s.Cfg.Sumdb.Enabled, |
| 100 | "HasAuth": len(s.AuthTokens) > 0, |
| 101 | } |
| 102 | |
| 103 | if s.ModuleLister != nil { |
| 104 | modules, _ := s.ModuleLister.ListModules() |
| 105 | patterns, _ := s.ModuleLister.ListPatterns() |
| 106 | |
| 107 | // Filter to public modules only. |
| 108 | var publicModules []map[string]string |
| 109 | for _, m := range modules { |
| 110 | if !m.Private { |
| 111 | publicModules = append(publicModules, map[string]string{ |
| 112 | "Name": m.Name, |
| 113 | "Repo": m.Repo, |
| 114 | "Web": m.Web, |
| 115 | }) |
| 116 | } |
| 117 | } |
| 118 | data["Modules"] = publicModules |
| 119 | |
| 120 | // Filter to public patterns only. |
| 121 | var publicPatterns []map[string]string |
| 122 | for _, p := range patterns { |
| 123 | if !p.Private { |
| 124 | publicPatterns = append(publicPatterns, map[string]string{ |
| 125 | "Pattern": p.Pattern, |
| 126 | "Repo": p.Repo, |
| 127 | }) |
| 128 | } |
| 129 | } |
| 130 | data["Patterns"] = publicPatterns |
| 131 | } |
| 132 | |
| 133 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 134 | if err := landingTmpl.Execute(w, data); err != nil { |
| 135 | log.Printf("server: render landing: %v", err) |
| 136 | } |
| 137 | } |
| 138 | |