diff --git a/src/auth.go b/src/auth.go index b0a1d86..2119a28 100644 --- a/src/auth.go +++ b/src/auth.go @@ -240,6 +240,72 @@ func authorizeWildcardMatchSite(r *http.Request, pattern *WildcardPattern) (*Aut } } +// used for compatibility with Codeberg Pages v2 +// see https://docs.codeberg.org/codeberg-pages/using-custom-domain/ +func authorizeCodebergPagesV2(r *http.Request) (*Authorization, error) { + host, err := GetHost(r) + if err != nil { + return nil, err + } + + dnsRecords := []string{} + + cnameRecord, err := net.LookupCNAME(host) + // "LookupCNAME does not return an error if host does not contain DNS "CNAME" records, + // as long as host resolves to address records. + if err == nil && cnameRecord != host { + // LookupCNAME() returns a domain with the root label, i.e. `username.codeberg.page.`, + // with the trailing dot + dnsRecords = append(dnsRecords, strings.TrimSuffix(cnameRecord, ".")) + } + + txtRecords, err := net.LookupTXT(host) + if err == nil { + dnsRecords = append(dnsRecords, txtRecords...) + } + + if len(dnsRecords) > 0 { + log.Printf("auth: %s TXT/CNAME: %v\n", host, dnsRecords) + } + + for _, dnsRecord := range dnsRecords { + domainParts := strings.Split(dnsRecord, ".") + slices.Reverse(domainParts) + if len(domainParts) >= 3 && len(domainParts) <= 5 { + if domainParts[0] == "page" && domainParts[1] == "codeberg" { + // map of domain names to allowed repository and branch: + // * {username}.codeberg.page => + // https://codeberg.org/{username}/pages.git#main + // * {reponame}.{username}.codeberg.page => + // https://codeberg.org/{username}/{reponame}.git#pages + // * {branch}.{reponame}.{username}.codeberg.page => + // https://codeberg.org/{username}/{reponame}.git#{branch} + username := domainParts[2] + reponame := "pages" + branch := "main" + if len(domainParts) >= 4 { + reponame = domainParts[3] + branch = "pages" + } + if len(domainParts) == 5 { + branch = domainParts[4] + } + return &Authorization{ + repoURLs: []string{ + fmt.Sprintf("https://codeberg.org/%s/%s.git", username, reponame), + }, + branch: branch, + }, nil + } + } + } + + return nil, AuthError{ + http.StatusUnauthorized, + fmt.Sprintf("domain %s does not have Codeberg Pages TXT or CNAME records", host), + } +} + func AuthorizeMetadataRetrieval(r *http.Request) (*Authorization, error) { causes := []error{AuthError{http.StatusUnauthorized, "unauthorized"}} @@ -325,6 +391,19 @@ func AuthorizeUpdateFromRepository(r *http.Request) (*Authorization, error) { return auth, nil } } + + if config.Feature("codeberg-pages-compat") { + auth, err = authorizeCodebergPagesV2(r) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + log.Printf("auth: codeberg %s: allow %v branch %s\n", + r.Host, auth.repoURLs, auth.branch) + return auth, nil + } + } } return nil, errors.Join(causes...) diff --git a/src/pages.go b/src/pages.go index 4965798..16d21cf 100644 --- a/src/pages.go +++ b/src/pages.go @@ -412,9 +412,11 @@ func postPage(w http.ResponseWriter, r *http.Request) error { } eventRef := event["ref"].(string) - if eventRef != "refs/heads/pages" { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "ignored %s\n", eventRef) + if eventRef != fmt.Sprintf("refs/heads/%s", auth.branch) { + http.Error(w, + fmt.Sprintf("ref %s not in allowlist [refs/heads/%v])", + eventRef, auth.branch), + http.StatusUnauthorized) return nil } @@ -425,7 +427,7 @@ func postPage(w http.ResponseWriter, r *http.Request) error { ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout)) defer cancel() - result := UpdateFromRepository(ctx, webRoot, repoURL, "pages") + result := UpdateFromRepository(ctx, webRoot, repoURL, auth.branch) switch result.outcome { case UpdateError: w.WriteHeader(http.StatusServiceUnavailable)