~gitpocket/gitpocket

8

feat(domains): point custom domains by cname and split the ssh host

commit
a289514c9abfecce583a90db2f01c87f2dc0fdf8 browse tree
parent
e0cad84176f3a107a44ea141a76ab87d56afcacf
author
~james <354576047+[email protected]>
signature
Signed by: ~james SHA256:KlzJ0P0gvJcnOYpaxD5yCUw52ImHttTKRjVAEnnYnKg
date
2026-11-14 10:27 UTC

6 files changed, +16 −32

forge/cli.go modified

@@ -229,6 +229,7 @@ func serve(root string, args []string) {
229229 host := fs.String("host", "localhost", "public hostname used in clone urls")
230230 name := fs.String("name", "gitpocket", "site name shown in the header")
231231 sshUser := fs.String("ssh-user", "git", "unix user that serves git over ssh, shown in clone urls")
232+ sshHost := fs.String("ssh-host", "", "hostname shown in ssh clone urls when it differs from -host")
232233 proxies := fs.String("proxy", "", "comma separated networks besides loopback whose X-Forwarded-For is trusted")
233234 fs.Parse(args)
234235 for _, cidr := range strings.Split(*proxies, ",") {
@@ -243,6 +244,9 @@ func serve(root string, args []string) {
243244 }
244245 store := mustStore(root)
245246 site := newServer(store, root, *host, *name, *sshUser)
247+ if *sshHost != "" {
248+ site.sshHost = *sshHost
249+ }
246250 go site.mirrorLoop()
247251 go site.domainLoop()
248252 srv := &http.Server{

forge/domain.go modified

@@ -124,19 +124,9 @@ func (s *Server) checkDomain(d Domain) error {
124124 if err != nil || !slices.Contains(txt, d.Token) {
125125 return fmt.Errorf("The TXT record %s does not contain %s yet", d.Record(), d.Token)
126126 }
127- want, err := net.DefaultResolver.LookupIPAddr(ctx, s.host)
128- if err != nil || len(want) == 0 {
129- return errors.New("Could not resolve " + s.host)
130- }
131- got, err := net.DefaultResolver.LookupIPAddr(ctx, d.Name)
132- if err != nil || len(got) == 0 {
127+ if got, err := net.DefaultResolver.LookupHost(ctx, d.Name); err != nil || len(got) == 0 {
133128 return fmt.Errorf("%s does not resolve yet", d.Name)
134129 }
135- for _, ip := range got {
136- if !slices.ContainsFunc(want, func(w net.IPAddr) bool { return w.IP.Equal(ip.IP) }) {
137- return fmt.Errorf("%s resolves to %s, point it at %s instead", d.Name, ip.IP, s.host)
138- }
139- }
140130 return nil
141131 }
142132
@@ -203,25 +193,13 @@ func (s *Server) setDomain(w http.ResponseWriter, r *http.Request) {
203193 }
204194
205195 func domainView(s *Server, u *User) map[string]any {
206- view := map[string]any{"Enabled": s.pagesEnabled(*u)}
196+ view := map[string]any{"Enabled": s.pagesEnabled(*u), "Target": s.host}
207197 if pagesLabel.MatchString(u.Name) && !reservedUsers[u.Name] {
208198 view["Subdomain"] = u.Name + "." + s.host
199+ view["Target"] = view["Subdomain"]
209200 }
210201 if d, ok := s.store.UserDomain(u.ID); ok {
211202 view["Domain"] = d
212203 }
213- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
214- defer cancel()
215- if ips, err := net.DefaultResolver.LookupIPAddr(ctx, s.host); err == nil {
216- var addrs [][2]string
217- for _, ip := range ips {
218- kind := "A"
219- if ip.IP.To4() == nil {
220- kind = "AAAA"
221- }
222- addrs = append(addrs, [2]string{kind, ip.IP.String()})
223- }
224- view["Addresses"] = addrs
225- }
226204 return view
227205 }

forge/web.go modified

@@ -54,6 +54,7 @@ type Server struct {
5454 host string
5555 name string
5656 sshUser string
57+ sshHost string
5758 pages map[string]*template.Template
5859 brand map[string][]byte
5960 markdown goldmark.Markdown
@@ -68,6 +69,7 @@ type page struct {
6869 Host string
6970 Name string
7071 SSHUser string
72+ SSHHost string
7173 Error string
7274 Math bool
7375 Data any
@@ -82,7 +84,7 @@ type page struct {
8284 }
8385
8486 func newServer(store *Store, root, host, name string, sshUser string) *Server {
85- s := &Server{store: store, root: root, host: host, name: name, sshUser: sshUser, mailer: loadMailer(root), pages: map[string]*template.Template{}, limiter: newLimiter(10, 5*time.Minute), requests: newBuckets(5, 120), heavy: newBuckets(0.5, 20), uploads: newLimiter(20, time.Hour), posts: newBuckets(0.2, 60)}
87+ s := &Server{store: store, root: root, host: host, name: name, sshUser: sshUser, sshHost: host, mailer: loadMailer(root), pages: map[string]*template.Template{}, limiter: newLimiter(10, 5*time.Minute), requests: newBuckets(5, 120), heavy: newBuckets(0.5, 20), uploads: newLimiter(20, time.Hour), posts: newBuckets(0.2, 60)}
8688 funcs := template.FuncMap{
8789 "short": func(h string) string {
8890 if len(h) > 10 {
@@ -175,6 +177,7 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, p p
175177 p.Host = s.host
176178 p.Name = s.name
177179 p.SSHUser = s.sshUser
180+ p.SSHHost = s.sshHost
178181 p.Version = s.version
179182 p.Logo = s.brand["logo.png"] != nil
180183 p.Icon = s.brand["icon.png"] != nil

web/templates/keys.tmpl modified

@@ -4,7 +4,7 @@
44 <form method="post" action="/settings/keys" class="stack">
55 <label>SSH public key <textarea name="key" rows="3" required placeholder="ssh-ed25519 AAAA..."></textarea></label>
66 <label>Title <input name="title" placeholder="Defaults to the key comment"></label>
7-<small>Push with <code>{{.SSHUser}}@{{.Host}}:~{{.User.Name}}/repo</code> once the key is registered. Your keys are public at <a href="/~{{.User.Name}}.keys">~{{.User.Name}}.keys</a>.</small>
7+<small>Push with <code>{{.SSHUser}}@{{.SSHHost}}:~{{.User.Name}}/repo</code> once the key is registered. Your keys are public at <a href="/~{{.User.Name}}.keys">~{{.User.Name}}.keys</a>.</small>
88 <div class="actions"><button class="btn primary">Add key ▸</button></div>
99 </form></div>
1010 <div class="panel"><h3>PGP keys</h3>

web/templates/repo.tmpl modified

@@ -3,7 +3,7 @@
33 {{template "pushed" .Data}}
44 {{if $p.Empty}}
55 <p>This repository is empty. Push something to it:</p>
6-<pre>git remote add origin {{.SSHUser}}@{{.Host}}:{{$p.Repo.Path}}
6+<pre>git remote add origin {{.SSHUser}}@{{.SSHHost}}:{{$p.Repo.Path}}
77 git push -u origin {{$p.Ref}}</pre>
88 {{else}}
99 <div class="summary">
@@ -23,7 +23,7 @@ git push -u origin {{$p.Ref}}</pre>
2323 <strong>read only</strong>
2424 <div><a href="https://{{.Host}}/{{$p.Repo.Path}}">https://{{.Host}}/{{$p.Repo.Path}}</a></div>
2525 {{if $p.Owner}}<strong>read/write</strong>
26-<div>{{.SSHUser}}@{{.Host}}:{{$p.Repo.Path}}</div>{{end}}
26+<div>{{.SSHUser}}@{{.SSHHost}}:{{$p.Repo.Path}}</div>{{end}}
2727 <strong>download</strong>
2828 <div><a href="/{{$p.Repo.Path}}/archive/{{$p.Ref}}.tar.gz">{{$p.Ref}}.tar.gz</a> <a class="muted small" href="/{{$p.Repo.Path}}/archive/{{$p.Ref}}.tar.gz.sha256" title="byte for byte reproducible for a given commit">sha256</a> · <a href="/{{$p.Repo.Path}}/archive/{{$p.Ref}}.zip">{{$p.Ref}}.zip</a></div>
2929 <div><a href="/{{$p.Repo.Path}}/feed">atom feed</a> · <a href="/{{$p.Repo.Path}}/compare">compare</a></div>

web/templates/settings.tmpl modified

@@ -53,10 +53,9 @@
5353 <p>Custom domain <code>{{.Name}}</code> {{if .Verified}}<span class="muted">(verified, serving your site)</span>{{else}}<span class="muted">(waiting for DNS)</span>{{end}}</p>
5454 {{if not .Verified}}<table class="keys">
5555 <tr><td><code>TXT</code></td><td><code>{{.Record}}</code></td><td><code>{{.Token}}</code></td></tr>
56-{{if $d.Subdomain}}<tr><td><code>CNAME</code></td><td><code>{{.Name}}</code></td><td><code>{{$d.Subdomain}}</code> <span class="muted small">for a subdomain</span></td></tr>{{end}}
57-{{$name := .Name}}{{range $d.Addresses}}<tr><td><code>{{index . 0}}</code></td><td><code>{{$name}}</code></td><td><code>{{index . 1}}</code></td></tr>{{end}}
56+<tr><td><code>CNAME</code></td><td><code>{{.Name}}</code></td><td><code>{{$d.Target}}</code></td></tr>
5857 </table>
59-<p class="muted small">Add the TXT record, then point the domain at this server with either the CNAME or the address records, unproxied. HTTPS is issued automatically a few minutes after verification.</p>{{end}}
58+<p class="muted small">Add the TXT record, then point the domain at <code>{{$d.Target}}</code> with a CNAME, which at the apex of a zone means CNAME flattening or an ALIAS record. HTTPS is issued at the edge a few minutes after verification and the address of the server is never needed.</p>{{end}}
6059 <form method="post" action="/settings/pages" class="inline">{{if not .Verified}}<button class="btn primary" name="action" value="verify">Verify ▸</button>{{end}}<button class="btn danger" name="action" value="remove">Remove domain</button></form>
6160 {{else}}
6261 <form method="post" action="/settings/pages" class="stack wide">