internal/control/pagescmd.go
105 lines · 3621 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "regexp"
8 "strings"
9
10 "gitbay.org/gitbay/internal/policy"
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func init() {
16 register(Command{Path: []string{"repo", "domain", "add"},
17 Summary: "serve pages on a custom domain: repo domain add <owner/name> <domain>", Run: runDomainAdd})
18 register(Command{Path: []string{"repo", "domain", "remove"},
19 Summary: "remove a custom pages domain: repo domain remove <owner/name> <domain>", Run: runDomainRemove})
20 register(Command{Path: []string{"repo", "domain", "list"},
21 Summary: "list custom pages domains: repo domain list <owner/name>", ReadOnly: true, Run: runDomainList})
22}
23
24// hostnamePat is a conservative DNS hostname: dot-separated labels,
25// lowercase, at least two labels.
26var hostnamePat = regexp.MustCompile(`^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
27
28func validatePageDomain(c *Ctx, domain string) error {
29 if !hostnamePat.MatchString(domain) {
30 return fmt.Errorf("invalid domain %q: lowercase hostname like docs.example.org", domain)
31 }
32 if domain == c.Cfg.SiteHost() || strings.HasSuffix(c.Cfg.SiteHost(), "."+domain) {
33 return errors.New("that is the forge's own host: pages content must stay off its origin")
34 }
35 if pd := c.Cfg.Pages.Domain; pd != "" && (domain == pd || strings.HasSuffix(domain, "."+pd)) {
36 return fmt.Errorf("%s is under the built-in pages domain; it is served automatically", domain)
37 }
38 return nil
39}
40
41func runDomainAdd(c *Ctx, args []string) int {
42 if len(args) != 2 {
43 return c.fail(protocol.ExitUsage, "usage: repo domain add <owner/name> <domain>")
44 }
45 domain := strings.ToLower(args[1])
46 if err := validatePageDomain(c, domain); err != nil {
47 return c.fail(protocol.ExitUsage, "%v", err)
48 }
49 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
50 if code >= 0 {
51 return code
52 }
53 if repo.Visibility != "public" {
54 return c.fail(protocol.ExitUsage, "pages serve public repositories only; %s is private", repo.Path())
55 }
56 if err := c.Store.AddPageDomain(domain, repo.ID); err != nil {
57 if errors.Is(err, store.ErrExists) {
58 // Not naming the holder: domain claims must not enumerate repos.
59 return c.fail(protocol.ExitUsage, "%s is already claimed on this instance", domain)
60 }
61 return c.fail(protocol.ExitFailure, "%v", err)
62 }
63 return c.emit(map[string]string{"domain": domain}, func(w io.Writer) {
64 fmt.Fprintf(w, "%s now serves %s's pages branch — point its DNS (A/AAAA) at this server\n", domain, repo.Path())
65 })
66}
67
68func runDomainRemove(c *Ctx, args []string) int {
69 if len(args) != 2 {
70 return c.fail(protocol.ExitUsage, "usage: repo domain remove <owner/name> <domain>")
71 }
72 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
73 if code >= 0 {
74 return code
75 }
76 domain := strings.ToLower(args[1])
77 if err := c.Store.RemovePageDomain(domain, repo.ID); err != nil {
78 if errors.Is(err, store.ErrNotFound) {
79 return c.fail(protocol.ExitNotFound, "%s is not a domain of %s", domain, repo.Path())
80 }
81 return c.fail(protocol.ExitFailure, "%v", err)
82 }
83 return c.emit(map[string]string{"removed": domain}, func(w io.Writer) {
84 fmt.Fprintf(w, "removed %s\n", domain)
85 })
86}
87
88func runDomainList(c *Ctx, args []string) int {
89 if len(args) != 1 {
90 return c.fail(protocol.ExitUsage, "usage: repo domain list <owner/name>")
91 }
92 repo, code := resolveRepo(c, args[0], policy.CanRead)
93 if code >= 0 {
94 return code
95 }
96 ds, err := c.Store.ListPageDomains(repo.ID)
97 if err != nil {
98 return c.fail(protocol.ExitFailure, "%v", err)
99 }
100 return c.emit(ds, func(w io.Writer) {
101 for _, d := range ds {
102 fmt.Fprintln(w, d)
103 }
104 })
105}