1// SPDX-License-Identifier: MIT
2
3package host
4
5import (
6 "encoding/json"
7 "strings"
8 "testing"
9
10 ermite "bnema.dev/ermite-api/gen/ermite"
11
12 "bnema.dev/ermite-runner/internal/execstream"
13 "bnema.dev/ermite-runner/internal/execstream/execpb"
14)
15
16const publicContexts = `{"github":{"job":"","ref":"refs/heads/main","repository":"octo/hello","repositoryUrl":"git://ermite.bnema.dev/octo/hello.git",` +
17 `"server_url":"https://ermite.bnema.dev","api_url":"https://ermite.bnema.dev/-/github/api","graphql_url":"https://ermite.bnema.dev/-/github/api/graphql",` +
18 `"event":{"compare":"https://ermite.bnema.dev/octo/hello/compare/a...b","repository":{"html_url":"https://ermite.bnema.dev/octo/hello"}},` +
19 `"note":"a & <b>"},"needs":{},"vars":{"U":"https://ermite.bnema.dev/x"}}`
20
21func TestRewriteServerURLs(t *testing.T) {
22 got, err := RewriteServerURLs(publicContexts, "http://ermite.bnema.dev:8088")
23 if err != nil {
24 t.Fatal(err)
25 }
26 want := strings.NewReplacer(
27 `"server_url":"https://ermite.bnema.dev"`, `"server_url":"http://ermite.bnema.dev:8088"`,
28 `"api_url":"https://ermite.bnema.dev/-/github/api"`, `"api_url":"http://ermite.bnema.dev:8088/-/github/api"`,
29 `"graphql_url":"https://ermite.bnema.dev/-/github/api/graphql"`, `"graphql_url":"http://ermite.bnema.dev:8088/-/github/api/graphql"`,
30 ).Replace(publicContexts)
31 // Only the three fields change: same bytes everywhere else (key order, the
32 // event payload as recorded, other URLs, escaping).
33 if got != want {
34 t.Fatalf("got\n%s\nwant\n%s", got, want)
35 }
36 if !json.Valid([]byte(got)) {
37 t.Fatal("invalid JSON")
38 }
39}
40
41func TestRewriteServerURLsMatchesOriginsOnly(t *testing.T) {
42 const target = "http://forge.internal:8088"
43 cases := map[string]struct {
44 in string
45 same bool
46 }{
47 "other origin is untouched": {`{"github":{"server_url":"https://ermite.bnema.dev","api_url":"https://api.example.org/x","graphql_url":"https://ermite.bnema.dev:8443/g"}}`, false},
48 "host prefix is not a match": {`{"github":{"server_url":"https://ermite.bnema.dev","api_url":"https://ermite.bnema.dev.evil.test/x","graphql_url":"https://xermite.bnema.dev/g"}}`, false},
49 "no server_url": {`{"github":{"api_url":"https://ermite.bnema.dev/-/github/api"}}`, true},
50 "no github": {`{"vars":{"a":"b"}}`, true},
51 "empty server_url": {`{"github":{"server_url":"","api_url":"https://ermite.bnema.dev/a"}}`, true},
52 "non-string values": {`{"github":{"server_url":"https://ermite.bnema.dev","api_url":null,"graphql_url":3}}`, false},
53 "already the target": {`{"github":{"server_url":"http://forge.internal:8088","api_url":"http://forge.internal:8088/a"}}`, true},
54 "credentials are not origins": {`{"github":{"server_url":"https://ermite.bnema.dev","api_url":"https://u:p@ermite.bnema.dev/a"}}`, false},
55 }
56 for name, c := range cases {
57 got, err := RewriteServerURLs(c.in, target)
58 if err != nil {
59 t.Fatalf("%s: %v", name, err)
60 }
61 if c.same && got != c.in {
62 t.Errorf("%s: changed:\n%s", name, got)
63 }
64 var v struct {
65 GitHub map[string]any `json:"github"`
66 }
67 if err := json.Unmarshal([]byte(got), &v); err != nil {
68 t.Fatalf("%s: %v", name, err)
69 }
70 switch name {
71 case "other origin is untouched", "host prefix is not a match", "credentials are not origins":
72 if v.GitHub["server_url"] != target {
73 t.Errorf("%s: server_url %v", name, v.GitHub["server_url"])
74 }
75 if u, _ := v.GitHub["api_url"].(string); strings.HasPrefix(u, target) {
76 t.Errorf("%s: api_url rewritten: %s", name, u)
77 }
78 if u, _ := v.GitHub["graphql_url"].(string); strings.HasPrefix(u, target) {
79 t.Errorf("%s: graphql_url rewritten: %s", name, u)
80 }
81 case "non-string values":
82 if v.GitHub["server_url"] != target || v.GitHub["api_url"] != nil || v.GitHub["graphql_url"] != float64(3) {
83 t.Errorf("%s: %v", name, v.GitHub)
84 }
85 }
86 }
87}
88
89func TestRewriteServerURLsSpellings(t *testing.T) {
90 // Case of scheme and host and a default port do not change the origin.
91 in := `{"github":{"server_url":"HTTPS://Ermite.BNEMA.dev:443/","api_url":"https://ermite.bnema.dev/-/github/api?x=1#f"}}`
92 got, err := RewriteServerURLs(in, "https://forge.test")
93 if err != nil {
94 t.Fatal(err)
95 }
96 if got != `{"github":{"server_url":"https://forge.test/","api_url":"https://forge.test/-/github/api?x=1#f"}}` {
97 t.Fatal(got)
98 }
99 // Off: the contexts are returned as received, valid or not.
100 if got, err := RewriteServerURLs("not json", ""); err != nil || got != "not json" {
101 t.Fatalf("%q %v", got, err)
102 }
103 for _, bad := range []string{"not json", `[1]`, `{"github":[1]}`, `{"github":{}} x`} {
104 if _, err := RewriteServerURLs(bad, "http://a.test"); err == nil {
105 t.Errorf("%q accepted", bad)
106 }
107 }
108}
109
110func TestOriginOf(t *testing.T) {
111 for in, want := range map[string]string{
112 "https://Ermite.example/x": "https://ermite.example", "http://a.test:80": "http://a.test",
113 "http://a.test:8088/p": "http://a.test:8088", "https://a.test:443": "https://a.test",
114 "ftp://a.test": "", "a.test": "", "https://u@a.test": "", "": "",
115 // GO-012: an IPv6 host stays in brackets, and the host "::1:8443" is not the
116 // host "::1" on port 8443.
117 "https://[::1]:8443/x": "https://[::1]:8443", "https://[::1:8443]/x": "https://[::1:8443]", "https://[::1]:443/": "https://[::1]",
118 "http://[2001:DB8::1]:80": "http://[2001:db8::1]",
119 } {
120 if got := originOf(in); got != want {
121 t.Errorf("originOf(%q) = %q, want %q", in, got, want)
122 }
123 }
124}
125
126// The executor receives the rewritten contexts when the access is on, and the
127// recorded ones when it is off.
128func TestJobContextsAreRewrittenOnlyWithGuestForge(t *testing.T) {
129 for _, on := range []bool{true, false} {
130 h := newHarness(t, simpleScript("x"))
131 job := testJob("j1")
132 job.Contexts = publicContexts
133 h.srv.jobs = []*ermite.Job{job}
134 if on {
135 h.runner.Cfg.GuestForgeOrigin = "http://ermite.bnema.dev:8088"
136 }
137 var seen string
138 inner := h.driver.script
139 h.driver.script = func(t *testing.T, c *execstream.Conn, run *execpb.Run, cn <-chan *execpb.Cancel) {
140 seen = run.Job.Contexts
141 inner(t, c, run, cn)
142 }
143 h.run(t)
144 eventually(t, "CompleteJob", func() bool { return h.srv.completeCount() == 1 })
145 if got := strings.Contains(seen, `"server_url":"http://ermite.bnema.dev:8088"`); got != on {
146 t.Errorf("guest forge %v: contexts %s", on, seen)
147 }
148 if !on && seen != publicContexts {
149 t.Errorf("contexts changed while off: %s", seen)
150 }
151 }
152}
153
154// GO-012: guest forge access on an IPv6 address. The URLs of the contexts are
155// rewritten to the runner's origin, the brackets and the port kept, and the
156// origin of the other host with the look-alike spelling is not touched.
157func TestRewriteServerURLsIPv6(t *testing.T) {
158 in := `{"github":{"server_url":"https://[::1]:8443","api_url":"https://[::1]:8443/-/github/api","graphql_url":"https://[::1:8443]/x"}}`
159 got, err := RewriteServerURLs(in, "http://[fd00::2]:8088")
160 if err != nil {
161 t.Fatal(err)
162 }
163 want := `{"github":{"server_url":"http://[fd00::2]:8088","api_url":"http://[fd00::2]:8088/-/github/api","graphql_url":"https://[::1:8443]/x"}}`
164 if got != want {
165 t.Errorf("got %s\nwant %s", got, want)
166 }
167}