config: optional baseurl field, the canonical outside address
commit 4eed84cb5356d5f47e14d9cab361734d928b253c · parent ef8eddc0ca
· browse this tree
Ximianth, 2026-10-27 17:05:35 UTC
Validated at load (absolute http(s) URL, host only) and normalized to scheme://host with no trailing slash. Nothing consumes it yet; the go-get vanity-import answers and the clone-URL display come next.
diff --git a/config.go b/config.go index 11e9040..7422224 100644 --- a/config.go +++ b/config.go @@ -23,6 +23,13 @@ type repoConfig struct { // config is the top-level schema of the socle config file. type config struct { + // BaseURL is the canonical outside address of this socle, e.g. "https://code.example": + // scheme and host only, since socle's URL space is rooted at "/". + // When set, it becomes the displayed clone URL and enables the go-get vanity-import + // answers (see serveGoGetMeta). When empty, clone URLs derive from each request + // and go-get is deliberately not served: socle should never claim an import path + // for a domain nobody chose. + BaseURL string `yaml:"baseurl"` // Repos maps URL names (which may contain slashes) to the repositories they serve. Repos map[string]repoConfig `yaml:"repos"` } @@ -47,6 +54,13 @@ func loadConfig(path string) (*config, error) { return nil, fmt.Errorf("config %s: %w", path, err) } + if cfg.BaseURL != "" { + normalized, err := validateBaseURL(cfg.BaseURL) + if err != nil { + return nil, fmt.Errorf("config %s: %w", path, err) + } + cfg.BaseURL = normalized + } if len(cfg.Repos) == 0 { return nil, fmt.Errorf("config %s: no repos configured; a socle with nothing on display is a misconfiguration", path) } diff --git a/config_test.go b/config_test.go index 7255cfd..cca67d5 100644 --- a/config_test.go +++ b/config_test.go @@ -66,6 +66,9 @@ func describeLoad(cfgPath, base string) string { return "error: " + anonymize(err.Error()) + "\n" } var sb strings.Builder + if cfg.BaseURL != "" { + fmt.Fprintf(&sb, "baseurl=%s\n", cfg.BaseURL) + } for _, name := range sortedNames(cfg.Repos) { rc := cfg.Repos[name] fmt.Fprintf(&sb, "repo %s: path=%s", name, anonymize(rc.Path)) diff --git a/testdata/config.md b/testdata/config.md index 9210cfc..3eac772 100644 --- a/testdata/config.md +++ b/testdata/config.md @@ -61,6 +61,87 @@ repo tidy: path=$BASE/sub/dir ``` +The base URL +------------ + +`baseurl` names the canonical outside address of this socle: +it becomes the displayed clone URL and enables the go-get vanity-import answers. +It is optional; without it, clone URLs derive from each request and go-get is not served. + +[testmark]:# (good-baseurl/config.yaml) +```yaml +baseurl: https://code.example +repos: + parnass: + path: "%" +``` + +[testmark]:# (good-baseurl/result) +```text +baseurl=https://code.example +repo parnass: path=$BASE/parnass +``` + +One trailing slash is tolerated and trimmed. + +[testmark]:# (good-baseurl-trailing-slash/config.yaml) +```yaml +baseurl: https://code.example/ +repos: + parnass: + path: "%" +``` + +[testmark]:# (good-baseurl-trailing-slash/result) +```text +baseurl=https://code.example +repo parnass: path=$BASE/parnass +``` + +It must be an absolute http(s) URL naming only a host: +no bare hostnames, no other schemes, and no path -- +socle's URL space is rooted at `/`, so a path prefix would promise routing it can't deliver. + +[testmark]:# (err-baseurl-relative/config.yaml) +```yaml +baseurl: code.example +repos: + parnass: + path: "%" +``` + +[testmark]:# (err-baseurl-relative/result) +```text +error: config $BASE/config.yaml: baseurl "code.example": must be an absolute URL with scheme http or https +``` + +[testmark]:# (err-baseurl-scheme/config.yaml) +```yaml +baseurl: ssh://code.example +repos: + parnass: + path: "%" +``` + +[testmark]:# (err-baseurl-scheme/result) +```text +error: config $BASE/config.yaml: baseurl "ssh://code.example": must be an absolute URL with scheme http or https +``` + +[testmark]:# (err-baseurl-path/config.yaml) +```yaml +baseurl: https://code.example/git +repos: + parnass: + path: "%" +``` + +[testmark]:# (err-baseurl-path/result) +```text +error: config $BASE/config.yaml: baseurl "https://code.example/git": must name only a host, with no path, query, fragment, or userinfo +``` + + Bad configs ----------- diff --git a/validate.go b/validate.go index 1fc805c..6813ecb 100644 --- a/validate.go +++ b/validate.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net/url" "strings" ) @@ -14,6 +15,22 @@ import ( // so what these guard against is git misparsing an argument: // options (leading dash), rev-parse operators (.. @{} ^ ~ :), and path traversal. +// validateBaseURL checks (and normalizes) the configured canonical base URL: +// an absolute http(s) URL naming only a host. One trailing slash is tolerated and trimmed; +// a path is refused rather than trimmed, because socle's URL space is rooted at "/" +// and a path prefix would promise routing it can't deliver. +// Returns the normalized form, scheme://host with no trailing slash. +func validateBaseURL(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil || u.Scheme != "http" && u.Scheme != "https" || u.Host == "" { + return "", fmt.Errorf("baseurl %q: must be an absolute URL with scheme http or https", raw) + } + if u.Path != "" && u.Path != "/" || u.RawQuery != "" || u.Fragment != "" || u.User != nil { + return "", fmt.Errorf("baseurl %q: must name only a host, with no path, query, fragment, or userinfo", raw) + } + return u.Scheme + "://" + u.Host, nil +} + // validateRepoName checks a configured repo name (which is also its URL prefix). // Names are slash-separated segments of [A-Za-z0-9._-], with no segment // empty, starting with a dot or a dash, or ending in ".git"