1package acceptance_test
2
3import (
4	"crypto/tls"
5	"fmt"
6	"io"
7	"net/http"
8	"net/http/httptest"
9	"net/url"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/require"
15)
16
17func TestArtifactProxyRequest(t *testing.T) {
18	transport := (TestHTTPSClient.Transport).(*http.Transport).Clone()
19	transport.ResponseHeaderTimeout = 5 * time.Second
20
21	content := "<!DOCTYPE html><html><head><title>Title of the document</title></head><body></body></html>"
22	contentLength := int64(len(content))
23	testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24		switch r.URL.RawPath {
25		case "/api/v4/projects/group%2Fproject/jobs/1/artifacts/delayed_200.html":
26			time.Sleep(2 * time.Second)
27			fallthrough
28		case "/api/v4/projects/group%2Fproject/jobs/1/artifacts/200.html",
29			"/api/v4/projects/group%2Fsubgroup%2Fproject/jobs/1/artifacts/200.html":
30			w.Header().Set("Content-Type", "text/html; charset=utf-8")
31			fmt.Fprint(w, content)
32		case "/api/v4/projects/group%2Fproject/jobs/1/artifacts/500.html":
33			w.Header().Set("Content-Type", "text/html; charset=utf-8")
34			w.WriteHeader(http.StatusInternalServerError)
35			fmt.Fprint(w, content)
36		default:
37			t.Logf("Unexpected r.URL.RawPath: %q", r.URL.RawPath)
38			w.Header().Set("Content-Type", "text/html; charset=utf-8")
39			w.WriteHeader(http.StatusNotFound)
40			fmt.Fprint(w, content)
41		}
42	}))
43
44	keyFile, certFile := CreateHTTPSFixtureFiles(t)
45	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
46	require.NoError(t, err)
47
48	testServer.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
49	testServer.StartTLS()
50
51	t.Cleanup(func() {
52		os.Remove(keyFile)
53		os.Remove(certFile)
54		testServer.Close()
55	})
56
57	tests := []struct {
58		name         string
59		host         string
60		path         string
61		status       int
62		content      string
63		length       int64
64		cacheControl string
65		contentType  string
66	}{
67		{
68			name:         "basic proxied request",
69			host:         "group.gitlab-example.com",
70			path:         "/-/project/-/jobs/1/artifacts/200.html",
71			status:       http.StatusOK,
72			content:      content,
73			length:       contentLength,
74			cacheControl: "max-age=3600",
75			contentType:  "text/html; charset=utf-8",
76		},
77		{
78			name:         "basic proxied request for subgroup",
79			host:         "group.gitlab-example.com",
80			path:         "/-/subgroup/project/-/jobs/1/artifacts/200.html",
81			status:       http.StatusOK,
82			content:      content,
83			length:       contentLength,
84			cacheControl: "max-age=3600",
85			contentType:  "text/html; charset=utf-8",
86		},
87		{
88			name:         "502 error while attempting to proxy",
89			host:         "group.gitlab-example.com",
90			path:         "/-/project/-/jobs/1/artifacts/delayed_200.html",
91			status:       http.StatusBadGateway,
92			content:      "",
93			length:       0,
94			cacheControl: "",
95			contentType:  "text/html; charset=utf-8",
96		},
97		{
98			name:         "Proxying 404 from server",
99			host:         "group.gitlab-example.com",
100			path:         "/-/project/-/jobs/1/artifacts/404.html",
101			status:       http.StatusNotFound,
102			content:      "",
103			length:       0,
104			cacheControl: "",
105			contentType:  "text/html; charset=utf-8",
106		},
107		{
108			name:         "Proxying 500 from server",
109			host:         "group.gitlab-example.com",
110			path:         "/-/project/-/jobs/1/artifacts/500.html",
111			status:       http.StatusInternalServerError,
112			content:      "",
113			length:       0,
114			cacheControl: "",
115			contentType:  "text/html; charset=utf-8",
116		},
117	}
118
119	// Ensure the IP address is used in the URL, as we're relying on IP SANs to
120	// validate
121	artifactServerURL := testServer.URL + "/api/v4"
122	t.Log("Artifact server URL", artifactServerURL)
123
124	args := []string{"-artifacts-server=" + artifactServerURL, "-artifacts-server-timeout=1"}
125
126	RunPagesProcess(t,
127		withListeners([]ListenSpec{httpListener}),
128		withArguments(args),
129		withEnv([]string{"SSL_CERT_FILE=" + certFile}),
130	)
131
132	for _, tt := range tests {
133		tt := tt
134		t.Run(tt.name, func(t *testing.T) {
135			t.Parallel()
136
137			resp, err := GetPageFromListener(t, httpListener, tt.host, tt.path)
138			require.NoError(t, err)
139			defer resp.Body.Close()
140
141			require.Equal(t, tt.status, resp.StatusCode)
142			require.Equal(t, tt.contentType, resp.Header.Get("Content-Type"))
143
144			if tt.status == http.StatusOK {
145				body, err := io.ReadAll(resp.Body)
146				require.NoError(t, err)
147				require.Equal(t, tt.content, string(body))
148				require.Equal(t, tt.length, resp.ContentLength)
149				require.Equal(t, tt.cacheControl, resp.Header.Get("Cache-Control"))
150			}
151		})
152	}
153}
154
155func TestPrivateArtifactProxyRequest(t *testing.T) {
156	setupTransport(t)
157
158	testServer := makeGitLabPagesAccessStub(t)
159
160	keyFile, certFile := CreateHTTPSFixtureFiles(t)
161	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
162	require.NoError(t, err)
163
164	testServer.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
165	testServer.StartTLS()
166
167	t.Cleanup(func() {
168		os.Remove(keyFile)
169		os.Remove(certFile)
170		testServer.Close()
171	})
172
173	tests := []struct {
174		name   string
175		host   string
176		path   string
177		status int
178	}{
179		{
180			name:   "basic proxied request for private project",
181			host:   "group.gitlab-example.com",
182			path:   "/-/private/-/jobs/1/artifacts/200.html",
183			status: http.StatusOK,
184		},
185		{
186			name:   "basic proxied request for subgroup",
187			host:   "group.gitlab-example.com",
188			path:   "/-/subgroup/private/-/jobs/1/artifacts/200.html",
189			status: http.StatusOK,
190		},
191		{
192			name:   "502 error while attempting to proxy",
193			host:   "group.gitlab-example.com",
194			path:   "/-/private/-/jobs/1/artifacts/delayed_200.html",
195			status: http.StatusBadGateway,
196		},
197		{
198			name:   "Proxying 404 from server",
199			host:   "group.gitlab-example.com",
200			path:   "/-/private/-/jobs/1/artifacts/404.html",
201			status: http.StatusNotFound,
202		},
203		{
204			name:   "Proxying 500 from server",
205			host:   "group.gitlab-example.com",
206			path:   "/-/private/-/jobs/1/artifacts/500.html",
207			status: http.StatusInternalServerError,
208		},
209	}
210
211	// Ensure the IP address is used in the URL, as we're relying on IP SANs to
212	// validate
213	artifactServerURL := testServer.URL + "/api/v4"
214	t.Log("Artifact server URL", artifactServerURL)
215
216	configFile := defaultConfigFileWith(t,
217		"gitlab-server="+testServer.URL,
218		"artifacts-server="+artifactServerURL,
219		"auth-redirect-uri=https://projects.gitlab-example.com/auth",
220		"artifacts-server-timeout=1")
221
222	RunPagesProcess(t,
223		withListeners([]ListenSpec{httpsListener}),
224		withArguments([]string{
225			"-config=" + configFile,
226		}),
227		withEnv([]string{"SSL_CERT_FILE=" + certFile}),
228	)
229
230	for _, tt := range tests {
231		tt := tt
232		t.Run(tt.name, func(t *testing.T) {
233			t.Parallel()
234
235			resp, err := GetRedirectPage(t, httpsListener, tt.host, tt.path)
236			require.NoError(t, err)
237			defer resp.Body.Close()
238
239			require.Equal(t, http.StatusFound, resp.StatusCode)
240
241			cookie := resp.Header.Get("Set-Cookie")
242
243			// Redirects to the projects under gitlab pages domain for authentication flow
244			url, err := url.Parse(resp.Header.Get("Location"))
245			require.NoError(t, err)
246			require.Equal(t, "projects.gitlab-example.com", url.Host)
247			require.Equal(t, "/auth", url.Path)
248			state := url.Query().Get("state")
249
250			resp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery)
251
252			require.NoError(t, err)
253			defer resp.Body.Close()
254
255			require.Equal(t, http.StatusFound, resp.StatusCode)
256			pagesDomainCookie := resp.Header.Get("Set-Cookie")
257
258			// Go to auth page with correct state will cause fetching the token
259			authrsp, err := GetRedirectPageWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+
260				state, pagesDomainCookie)
261
262			require.NoError(t, err)
263			defer authrsp.Body.Close()
264
265			// Will redirect auth callback to correct host
266			url, err = url.Parse(authrsp.Header.Get("Location"))
267			require.NoError(t, err)
268			require.Equal(t, tt.host, url.Host)
269			require.Equal(t, "/auth", url.Path)
270
271			// Request auth callback in project domain
272			authrsp, err = GetRedirectPageWithCookie(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery, cookie)
273			require.NoError(t, err)
274			defer authrsp.Body.Close()
275
276			// server returns the ticket, user will be redirected to the project page
277			require.Equal(t, http.StatusFound, authrsp.StatusCode)
278			cookie = authrsp.Header.Get("Set-Cookie")
279			resp, err = GetRedirectPageWithCookie(t, httpsListener, tt.host, tt.path, cookie)
280
281			require.Equal(t, tt.status, resp.StatusCode)
282
283			require.NoError(t, err)
284			defer resp.Body.Close()
285		})
286	}
287}
288