diff --git a/README.md b/README.md index 8da58f6..6b0294a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,11 @@ $ curl http://127.0.0.1:3333/ -H 'Host: codeberg.page' Authorization ------------- -DNS is used for authorization of content updates for custom domain names. Whenever a `PUT` or `POST` request is received at `hostname.tld` that has an `Authorization: Pages ` header, the TXT record(s) at `_git-pages-challenge.hostname.tld` are compared with `sha256("hostname.tld ")`. If there is a match then updates from any clone URLs are allowed. +DNS is used for authorization of content updates. + +- If a `[wildcard]` configuration section is specified, and if the suffix of a hostname in a `POST` request is equal to `[wildcard].domain`, then the request is authorized when and only when the repository URL in the event body matches the repository URL computed from the configuration file. Otherwise the next rule is used. + +- If a `PUT` or `POST` request is received at `` with an `Authorization: Pages ` header, then the request is authorized when any of the the TXT records at `_git-pages-challenge.` are equal to `SHA256(" ")`. Architecture diff --git a/config.toml.example b/config.toml.example index 0be048b..dd0ecbe 100644 --- a/config.toml.example +++ b/config.toml.example @@ -3,3 +3,8 @@ data-dir = "./data" [listen] protocol = "tcp" address = ":3333" + +[wildcard] +domain = "codeberg.page" +clone-url = "https://codeberg.org/%s/%s.git" +index-repo = "%s.codeberg.page" diff --git a/src/auth.go b/src/auth.go index 572df09..37955a8 100644 --- a/src/auth.go +++ b/src/auth.go @@ -9,7 +9,7 @@ import ( "strings" ) -func getHost(r *http.Request) string { +func GetHost(r *http.Request) string { // FIXME: handle IDNA host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -19,8 +19,8 @@ func getHost(r *http.Request) string { return host } -func authorize(w http.ResponseWriter, r *http.Request) error { - host := getHost(r) +func Authorize(w http.ResponseWriter, r *http.Request) error { + host := GetHost(r) authorization := r.Header.Get("Authorization") if authorization == "" { diff --git a/src/config.go b/src/config.go index c8ab78c..e2df137 100644 --- a/src/config.go +++ b/src/config.go @@ -12,6 +12,11 @@ type Config struct { Protocol string `toml:"protocol"` Address string `toml:"address"` } `toml:"listen"` + Wildcard struct { + Domain string `toml:"domain"` + CloneURL string `toml:"clone-url"` + IndexRepo string `toml:"index-repo"` + } `toml:"wildcard"` } func readConfig(path string, config *Config) error { diff --git a/src/serve.go b/src/serve.go index 424054e..20a2d4b 100644 --- a/src/serve.go +++ b/src/serve.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strings" "time" @@ -20,7 +21,7 @@ import ( const fetchTimeout = 30 * time.Second func getPage(w http.ResponseWriter, r *http.Request) error { - host := getHost(r) + host := GetHost(r) // if the first directory of the path exists under `www/$host`, use it as the root, // else use `www/$host/.index` @@ -104,9 +105,9 @@ func getProjectName(w http.ResponseWriter, r *http.Request) (string, error) { } func putPage(w http.ResponseWriter, r *http.Request) error { - host := getHost(r) + host := GetHost(r) - err := authorize(w, r) + err := Authorize(w, r) if err != nil { return err } @@ -156,18 +157,28 @@ func putPage(w http.ResponseWriter, r *http.Request) error { } func postPage(w http.ResponseWriter, r *http.Request) error { - host := getHost(r) - - err := authorize(w, r) - if err != nil { - return err - } + host := GetHost(r) + hostParts := strings.Split(host, ".") projectName, err := getProjectName(w, r) if err != nil { return err } + allowRepoURL := "" + if slices.Equal(hostParts[1:], strings.Split(config.Wildcard.Domain, ".")) { + userName := hostParts[0] + repoName := projectName + if repoName == ".index" { + repoName = fmt.Sprintf(config.Wildcard.IndexRepo, userName) + } + allowRepoURL = fmt.Sprintf(config.Wildcard.CloneURL, userName, repoName) + } else { + if err := Authorize(w, r); err != nil { + return err + } + } + if r.Header.Get("Content-Type") != "application/json" { http.Error(w, "only JSON payload is allowed", http.StatusBadRequest) return fmt.Errorf("invalid content type") @@ -197,7 +208,15 @@ func postPage(w http.ResponseWriter, r *http.Request) error { } webRoot := fmt.Sprintf("%s/%s", host, projectName) + repoURL := event["repository"].(map[string]any)["clone_url"].(string) + if allowRepoURL != "" && repoURL != allowRepoURL { + http.Error(w, + fmt.Sprintf("wildcard domain requires repository to be %s", allowRepoURL), + http.StatusUnauthorized, + ) + return fmt.Errorf("invalid clone URL") + } result := FetchWithTimeout(webRoot, repoURL, "pages", fetchTimeout) switch result.outcome {