[mirror] Scalable static site server for Git forges (like GitHub Pages)

Add a `relaxed-idna` feature to allow some uses of `_` in hostnames.

This is added to aid migration from Codeberg Pages v2. Forgejo allows
both `_` and `-` in usernames, and it is necessary to be able to accept
host names like `user_name.codeberg.page` under a wildcard domain.
(It is not possible to get a TLS certificate for a host name like this,
so only a wildcard certificate will be able to cover it.)

Changed files
+75 -4
.forgejo
workflows
src
+3
.forgejo/workflows/ci.yaml
··· 28 28 - name: Build service 29 29 run: | 30 30 go build 31 + - name: Run tests 32 + run: | 33 + go test ./src 31 34 - name: Run static analysis 32 35 run: | 33 36 go vet
+17 -4
src/auth.go
··· 54 54 // this also rejects invalid characters and labels 55 55 host, err = idnaProfile.ToASCII(host) 56 56 if err != nil { 57 - return "", AuthError{http.StatusBadRequest, 58 - fmt.Sprintf("malformed host name %q", host)} 57 + if config.Feature("relaxed-idna") { 58 + // unfortunately, the go IDNA library has some significant issues around its 59 + // Unicode TR46 implementation: https://github.com/golang/go/issues/76804 60 + // we would like to allow *just* the _ here, but adding `idna.StrictDomainName(false)` 61 + // would also accept domains like `*.foo.bar` which should clearly be disallowed. 62 + // as a workaround, accept a domain name if it is valid with all `_` characters 63 + // replaced with an alphanumeric character (we use `a`); this allows e.g. `foo_bar.xxx` 64 + // and `foo__bar.xxx`, as well as `_foo.xxx` and `foo_.xxx`. labels starting with 65 + // an underscore are explicitly rejected below. 66 + _, err = idnaProfile.ToASCII(strings.ReplaceAll(host, "_", "a")) 67 + } 68 + if err != nil { 69 + return "", AuthError{http.StatusBadRequest, 70 + fmt.Sprintf("malformed host name %q", host)} 71 + } 59 72 } 60 - if strings.HasPrefix(host, ".") { 73 + if strings.HasPrefix(host, ".") || strings.HasPrefix(host, "_") { 61 74 return "", AuthError{http.StatusBadRequest, 62 - fmt.Sprintf("host name %q is reserved", host)} 75 + fmt.Sprintf("reserved host name %q", host)} 63 76 } 64 77 host = strings.TrimSuffix(host, ".") 65 78 return host, nil
+55
src/pages_test.go
··· 1 + package git_pages 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + "testing" 7 + ) 8 + 9 + func checkHost(t *testing.T, host string, expectOk string, expectErr string) { 10 + host, err := GetHost(&http.Request{Host: host}) 11 + if expectErr != "" { 12 + if err == nil || !strings.HasPrefix(err.Error(), expectErr) { 13 + t.Errorf("%s: expect err %s, got err %s", host, expectErr, err) 14 + } 15 + } 16 + if expectOk != "" { 17 + if err != nil { 18 + t.Errorf("%s: expect ok %s, got err %s", host, expectOk, err) 19 + } else if host != expectOk { 20 + t.Errorf("%s: expect ok %s, got ok %s", host, expectOk, host) 21 + } 22 + } 23 + } 24 + 25 + func TestHelloName(t *testing.T) { 26 + config = &Config{Features: []string{}} 27 + 28 + checkHost(t, "foo.bar", "foo.bar", "") 29 + checkHost(t, "foo-baz.bar", "foo-baz.bar", "") 30 + checkHost(t, "foo--baz.bar", "foo--baz.bar", "") 31 + checkHost(t, "foo.bar.", "foo.bar", "") 32 + checkHost(t, ".foo.bar", "", "reserved host name") 33 + checkHost(t, "..foo.bar", "", "reserved host name") 34 + 35 + checkHost(t, "ß.bar", "xn--zca.bar", "") 36 + checkHost(t, "xn--zca.bar", "xn--zca.bar", "") 37 + 38 + checkHost(t, "foo-.bar", "", "malformed host name") 39 + checkHost(t, "-foo.bar", "", "malformed host name") 40 + checkHost(t, "foo_.bar", "", "malformed host name") 41 + checkHost(t, "_foo.bar", "", "malformed host name") 42 + checkHost(t, "foo_baz.bar", "", "malformed host name") 43 + checkHost(t, "foo__baz.bar", "", "malformed host name") 44 + checkHost(t, "*.foo.bar", "", "malformed host name") 45 + 46 + config = &Config{Features: []string{"relaxed-idna"}} 47 + 48 + checkHost(t, "foo-.bar", "", "malformed host name") 49 + checkHost(t, "-foo.bar", "", "malformed host name") 50 + checkHost(t, "foo_.bar", "foo_.bar", "") 51 + checkHost(t, "_foo.bar", "", "reserved host name") 52 + checkHost(t, "foo_baz.bar", "foo_baz.bar", "") 53 + checkHost(t, "foo__baz.bar", "foo__baz.bar", "") 54 + checkHost(t, "*.foo.bar", "", "malformed host name") 55 + }