feat(domains): point custom domains by cname and split the ssh host
6 files changed, +16 −32
- forge/cli.go modified +4 −0
- forge/domain.go modified +3 −25
- forge/web.go modified +4 −1
- web/templates/keys.tmpl modified +1 −1
- web/templates/repo.tmpl modified +2 −2
- web/templates/settings.tmpl modified +2 −3
forge/cli.go modified
| @@ -229,6 +229,7 @@ func serve(root string, args []string) { | ||
| 229 | 229 | host := fs.String("host", "localhost", "public hostname used in clone urls") |
| 230 | 230 | name := fs.String("name", "gitpocket", "site name shown in the header") |
| 231 | 231 | 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") | |
| 232 | 233 | proxies := fs.String("proxy", "", "comma separated networks besides loopback whose X-Forwarded-For is trusted") |
| 233 | 234 | fs.Parse(args) |
| 234 | 235 | for _, cidr := range strings.Split(*proxies, ",") { |
| @@ -243,6 +244,9 @@ func serve(root string, args []string) { | ||
| 243 | 244 | } |
| 244 | 245 | store := mustStore(root) |
| 245 | 246 | site := newServer(store, root, *host, *name, *sshUser) |
| 247 | + if *sshHost != "" { | |
| 248 | + site.sshHost = *sshHost | |
| 249 | + } | |
| 246 | 250 | go site.mirrorLoop() |
| 247 | 251 | go site.domainLoop() |
| 248 | 252 | srv := &http.Server{ |
forge/domain.go modified
| @@ -124,19 +124,9 @@ func (s *Server) checkDomain(d Domain) error { | ||
| 124 | 124 | if err != nil || !slices.Contains(txt, d.Token) { |
| 125 | 125 | return fmt.Errorf("The TXT record %s does not contain %s yet", d.Record(), d.Token) |
| 126 | 126 | } |
| 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 { | |
| 133 | 128 | return fmt.Errorf("%s does not resolve yet", d.Name) |
| 134 | 129 | } |
| 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 | - } | |
| 140 | 130 | return nil |
| 141 | 131 | } |
| 142 | 132 | |
| @@ -203,25 +193,13 @@ func (s *Server) setDomain(w http.ResponseWriter, r *http.Request) { | ||
| 203 | 193 | } |
| 204 | 194 | |
| 205 | 195 | 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} | |
| 207 | 197 | if pagesLabel.MatchString(u.Name) && !reservedUsers[u.Name] { |
| 208 | 198 | view["Subdomain"] = u.Name + "." + s.host |
| 199 | + view["Target"] = view["Subdomain"] | |
| 209 | 200 | } |
| 210 | 201 | if d, ok := s.store.UserDomain(u.ID); ok { |
| 211 | 202 | view["Domain"] = d |
| 212 | 203 | } |
| 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 | - } | |
| 226 | 204 | return view |
| 227 | 205 | } |
forge/web.go modified
| @@ -54,6 +54,7 @@ type Server struct { | ||
| 54 | 54 | host string |
| 55 | 55 | name string |
| 56 | 56 | sshUser string |
| 57 | + sshHost string | |
| 57 | 58 | pages map[string]*template.Template |
| 58 | 59 | brand map[string][]byte |
| 59 | 60 | markdown goldmark.Markdown |
| @@ -68,6 +69,7 @@ type page struct { | ||
| 68 | 69 | Host string |
| 69 | 70 | Name string |
| 70 | 71 | SSHUser string |
| 72 | + SSHHost string | |
| 71 | 73 | Error string |
| 72 | 74 | Math bool |
| 73 | 75 | Data any |
| @@ -82,7 +84,7 @@ type page struct { | ||
| 82 | 84 | } |
| 83 | 85 | |
| 84 | 86 | 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)} | |
| 86 | 88 | funcs := template.FuncMap{ |
| 87 | 89 | "short": func(h string) string { |
| 88 | 90 | if len(h) > 10 { |
| @@ -175,6 +177,7 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, p p | ||
| 175 | 177 | p.Host = s.host |
| 176 | 178 | p.Name = s.name |
| 177 | 179 | p.SSHUser = s.sshUser |
| 180 | + p.SSHHost = s.sshHost | |
| 178 | 181 | p.Version = s.version |
| 179 | 182 | p.Logo = s.brand["logo.png"] != nil |
| 180 | 183 | p.Icon = s.brand["icon.png"] != nil |
web/templates/keys.tmpl modified
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | <form method="post" action="/settings/keys" class="stack"> |
| 5 | 5 | <label>SSH public key <textarea name="key" rows="3" required placeholder="ssh-ed25519 AAAA..."></textarea></label> |
| 6 | 6 | <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> | |
| 8 | 8 | <div class="actions"><button class="btn primary">Add key ▸</button></div> |
| 9 | 9 | </form></div> |
| 10 | 10 | <div class="panel"><h3>PGP keys</h3> |
web/templates/repo.tmpl modified
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | {{template "pushed" .Data}} |
| 4 | 4 | {{if $p.Empty}} |
| 5 | 5 | <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}} | |
| 7 | 7 | git push -u origin {{$p.Ref}}</pre> |
| 8 | 8 | {{else}} |
| 9 | 9 | <div class="summary"> |
| @@ -23,7 +23,7 @@ git push -u origin {{$p.Ref}}</pre> | ||
| 23 | 23 | <strong>read only</strong> |
| 24 | 24 | <div><a href="https://{{.Host}}/{{$p.Repo.Path}}">https://{{.Host}}/{{$p.Repo.Path}}</a></div> |
| 25 | 25 | {{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}} | |
| 27 | 27 | <strong>download</strong> |
| 28 | 28 | <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> |
| 29 | 29 | <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 @@ | ||
| 53 | 53 | <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> |
| 54 | 54 | {{if not .Verified}}<table class="keys"> |
| 55 | 55 | <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> | |
| 58 | 57 | </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}} | |
| 60 | 59 | <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> |
| 61 | 60 | {{else}} |
| 62 | 61 | <form method="post" action="/settings/pages" class="stack wide"> |