diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index c52161b..0c8b80e 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -28,6 +28,9 @@ jobs: - name: Build service run: | go build + - name: Run tests + run: | + go test ./src - name: Run static analysis run: | go vet diff --git a/src/auth.go b/src/auth.go index ccc98fc..fd9d410 100644 --- a/src/auth.go +++ b/src/auth.go @@ -54,12 +54,25 @@ func GetHost(r *http.Request) (string, error) { // this also rejects invalid characters and labels host, err = idnaProfile.ToASCII(host) if err != nil { - return "", AuthError{http.StatusBadRequest, - fmt.Sprintf("malformed host name %q", host)} + if config.Feature("relaxed-idna") { + // unfortunately, the go IDNA library has some significant issues around its + // Unicode TR46 implementation: https://github.com/golang/go/issues/76804 + // we would like to allow *just* the _ here, but adding `idna.StrictDomainName(false)` + // would also accept domains like `*.foo.bar` which should clearly be disallowed. + // as a workaround, accept a domain name if it is valid with all `_` characters + // replaced with an alphanumeric character (we use `a`); this allows e.g. `foo_bar.xxx` + // and `foo__bar.xxx`, as well as `_foo.xxx` and `foo_.xxx`. labels starting with + // an underscore are explicitly rejected below. + _, err = idnaProfile.ToASCII(strings.ReplaceAll(host, "_", "a")) + } + if err != nil { + return "", AuthError{http.StatusBadRequest, + fmt.Sprintf("malformed host name %q", host)} + } } - if strings.HasPrefix(host, ".") { + if strings.HasPrefix(host, ".") || strings.HasPrefix(host, "_") { return "", AuthError{http.StatusBadRequest, - fmt.Sprintf("host name %q is reserved", host)} + fmt.Sprintf("reserved host name %q", host)} } host = strings.TrimSuffix(host, ".") return host, nil diff --git a/src/pages_test.go b/src/pages_test.go new file mode 100644 index 0000000..c9875f7 --- /dev/null +++ b/src/pages_test.go @@ -0,0 +1,55 @@ +package git_pages + +import ( + "net/http" + "strings" + "testing" +) + +func checkHost(t *testing.T, host string, expectOk string, expectErr string) { + host, err := GetHost(&http.Request{Host: host}) + if expectErr != "" { + if err == nil || !strings.HasPrefix(err.Error(), expectErr) { + t.Errorf("%s: expect err %s, got err %s", host, expectErr, err) + } + } + if expectOk != "" { + if err != nil { + t.Errorf("%s: expect ok %s, got err %s", host, expectOk, err) + } else if host != expectOk { + t.Errorf("%s: expect ok %s, got ok %s", host, expectOk, host) + } + } +} + +func TestHelloName(t *testing.T) { + config = &Config{Features: []string{}} + + checkHost(t, "foo.bar", "foo.bar", "") + checkHost(t, "foo-baz.bar", "foo-baz.bar", "") + checkHost(t, "foo--baz.bar", "foo--baz.bar", "") + checkHost(t, "foo.bar.", "foo.bar", "") + checkHost(t, ".foo.bar", "", "reserved host name") + checkHost(t, "..foo.bar", "", "reserved host name") + + checkHost(t, "ß.bar", "xn--zca.bar", "") + checkHost(t, "xn--zca.bar", "xn--zca.bar", "") + + checkHost(t, "foo-.bar", "", "malformed host name") + checkHost(t, "-foo.bar", "", "malformed host name") + checkHost(t, "foo_.bar", "", "malformed host name") + checkHost(t, "_foo.bar", "", "malformed host name") + checkHost(t, "foo_baz.bar", "", "malformed host name") + checkHost(t, "foo__baz.bar", "", "malformed host name") + checkHost(t, "*.foo.bar", "", "malformed host name") + + config = &Config{Features: []string{"relaxed-idna"}} + + checkHost(t, "foo-.bar", "", "malformed host name") + checkHost(t, "-foo.bar", "", "malformed host name") + checkHost(t, "foo_.bar", "foo_.bar", "") + checkHost(t, "_foo.bar", "", "reserved host name") + checkHost(t, "foo_baz.bar", "foo_baz.bar", "") + checkHost(t, "foo__baz.bar", "foo__baz.bar", "") + checkHost(t, "*.foo.bar", "", "malformed host name") +}