internal/control/pagescmd.go

0d12684707ba92a7a5dcf9aee6077df1f8c17c9c
gitbay/internal/control/pagescmd.go history · blame · raw

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}