landing.go

v1.4.3
Doc Versions Source
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;--red:#d93025;--max:900px}
17
@media(prefers-color-scheme:dark){:root{--bg:#1e1e1e;--bg2:#252526;--text:#d4d4d4;--text2:#a0a0a0;--border:#404040;--accent:#6cb6ff;--green:#81c995;--red:#f28b82}}
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
.topbar{background:var(--bg2);border-bottom:1px solid var(--border);padding:10px 0}
22
.topbar-inner{max-width:var(--max);margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between}
23
.topbar-left{font-weight:600;font-size:18px;color:var(--text);text-decoration:none}
24
.topbar-left:hover{text-decoration:none;opacity:0.8}
25
.topbar-right{display:flex;align-items:center;gap:8px;font-size:13px}
26
.topbar-right form{display:inline}
27
.topbar-btn{display:inline-block;padding:5px 14px;border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;color:var(--text);text-decoration:none;cursor:pointer;background:var(--bg)}
28
.topbar-btn:hover{background:var(--border);text-decoration:none}
29
.topbar-btn-muted{color:var(--text2)}
30
.container{max-width:var(--max);margin:0 auto;padding:24px}
31
.subtitle{color:var(--text2);font-size:16px;margin-bottom:32px}
32
h2{font-size:20px;margin:32px 0 12px}
33
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}
34
code{font-family:monospace;font-size:13px;background:var(--bg2);padding:2px 6px;border-radius:3px}
35
table{width:100%;border-collapse:collapse;margin-bottom:24px}
36
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}
37
td{padding:8px 12px;border-bottom:1px solid var(--border);font-size:14px}
38
td:first-child{font-family:monospace;font-size:13px}
39
.note{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px 16px;font-size:14px;color:var(--text2);margin-top:16px}
40
.badge-private{font-size:11px;color:var(--text2);border:1px solid var(--border);border-radius:3px;padding:1px 5px}
41
footer{text-align:center;padding:32px 0 16px;font-size:12px;color:var(--text2)}
42
</style>
43
</head>
44
<body>
45
<div class="topbar">
46
<div class="topbar-inner">
47
<a href="/" class="topbar-left">{{.Host}}</a>
48
<div class="topbar-right">
49
{{if .HasAuth}}
50
{{if .Authenticated}}
51
<a href="/-/admin/" class="topbar-btn">Admin</a>
52
<form method="POST" action="/-/logout"><button type="submit" class="topbar-btn topbar-btn-muted">Logout</button></form>
53
{{else}}
54
<a href="/-/admin/login?return={{.CurrentPath}}" class="topbar-btn">Login</a>
55
{{end}}
56
{{end}}
57
</div>
58
</div>
59
</div>
60
<div class="container">
61
62
<h2>Setup</h2>
63
<p>Configure your Go toolchain to use this proxy:</p>
64
<pre>export GOPROXY=https://{{.Host}},direct{{if not .SumdbEnabled}}
65
export GONOSUMDB={{.Host}}{{end}}</pre>
66
67
{{if .SumdbEnabled}}
68
<p>This server provides a <a href="/sumdb/{{.Host}}/latest">transparency log</a> (sumdb) for module checksums.</p>
69
{{end}}
70
71
<p>Then import modules as usual:</p>
72
<pre>import "{{.Host}}/&lt;module-name&gt;"</pre>
73
74
{{if .Modules}}
75
<h2>Modules</h2>
76
<table>
77
<thead>
78
<tr><th>Module</th><th>Repository</th></tr>
79
</thead>
80
<tbody>
81
{{range .Modules}}
82
<tr>
83
<td><a href="/{{.Name}}">{{$.Host}}/{{.Name}}</a>{{if .Private}} <span class="badge-private">private</span>{{end}}</td>
84
<td>{{if .Web}}<a href="{{.Web}}">{{.Web}}</a>{{else}}{{.Repo}}{{end}}</td>
85
</tr>
86
{{end}}
87
</tbody>
88
</table>
89
{{end}}
90
91
{{if .Patterns}}
92
<h2>Module Patterns</h2>
93
<p style="margin-bottom:12px;color:var(--text2);font-size:14px">Modules matching these patterns are served automatically.</p>
94
<table>
95
<thead>
96
<tr><th>Pattern</th><th>Repository Template</th></tr>
97
</thead>
98
<tbody>
99
{{range .Patterns}}
100
<tr>
101
<td><code>{{.Pattern}}</code></td>
102
<td>{{.Repo}}</td>
103
</tr>
104
{{end}}
105
</tbody>
106
</table>
107
{{end}}
108
109
{{if .HasAuth}}
110
<div class="note">Some modules are private and require authentication. Use a <code>Bearer</code> token or HTTP basic auth to access them.</div>
111
{{end}}
112
</div>
113
<footer>Powered by Curator</footer>
114
</body>
115
</html>
116
`))
117
118
func (s *Server) serveLanding(w http.ResponseWriter, r *http.Request) {
119
	authed := s.isAuthenticated(r)
120
	data := map[string]any{
121
		"Host":          s.Cfg.Host,
122
		"SumdbEnabled":  s.Cfg.Sumdb != nil && s.Cfg.Sumdb.Enabled,
123
		"HasAuth":       len(s.AuthTokens) > 0 || s.Cfg.AdminToken != "" || s.OIDCProvider != nil,
124
		"Authenticated": authed,
125
		"CurrentPath":   r.URL.Path,
126
	}
127
128
	if s.ModuleLister != nil {
129
		modules, _ := s.ModuleLister.ListModules()
130
		patterns, _ := s.ModuleLister.ListPatterns()
131
		authed := s.isAuthenticated(r)
132
133
		// Show public modules to everyone, private only to authenticated users.
134
		var visibleModules []map[string]string
135
		for _, m := range modules {
136
			if !m.Private || authed {
137
				entry := map[string]string{
138
					"Name": m.Name,
139
					"Repo": m.Repo,
140
					"Web":  m.Web,
141
				}
142
				if m.Private {
143
					entry["Private"] = "true"
144
				}
145
				visibleModules = append(visibleModules, entry)
146
			}
147
		}
148
		data["Modules"] = visibleModules
149
150
		var visiblePatterns []map[string]string
151
		for _, p := range patterns {
152
			if !p.Private || authed {
153
				entry := map[string]string{
154
					"Pattern": p.Pattern,
155
					"Repo":    p.Repo,
156
				}
157
				if p.Private {
158
					entry["Private"] = "true"
159
				}
160
				visiblePatterns = append(visiblePatterns, entry)
161
			}
162
		}
163
		data["Patterns"] = visiblePatterns
164
	}
165
166
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
167
	if err := landingTmpl.Execute(w, data); err != nil {
168
		log.Printf("server: render landing: %v", err)
169
	}
170
}
171
172
// ServeLogout handles POST /-/logout.
173
func (s *Server) ServeLogout(w http.ResponseWriter, r *http.Request) {
174
	http.SetCookie(w, &http.Cookie{
175
		Name:   "curator_auth",
176
		Path:   "/",
177
		MaxAge: -1,
178
	})
179
	http.SetCookie(w, &http.Cookie{
180
		Name:   "curator_admin",
181
		Path:   "/-/",
182
		MaxAge: -1,
183
	})
184
	http.SetCookie(w, &http.Cookie{
185
		Name:   "curator_session",
186
		Path:   "/-/",
187
		MaxAge: -1,
188
	})
189
	referer := r.Header.Get("Referer")
190
	if referer == "" {
191
		referer = "/"
192
	}
193
	http.Redirect(w, r, referer, http.StatusSeeOther)
194
}
195

Source Files