1package testdata
2
3import (
4	"crypto/sha256"
5	"encoding/hex"
6	"fmt"
7	"os"
8	"path/filepath"
9	"strings"
10	"testing"
11
12	"github.com/stretchr/testify/require"
13
14	"gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab/api"
15)
16
17type responseFn func(*testing.T, string) api.VirtualDomain
18
19// DomainResponses holds the predefined API responses for certain domains
20// that can be used with the GitLab API stub in acceptance tests
21// Assume the working dir is inside shared/pages/
22var DomainResponses = map[string]responseFn{
23	"zip-from-disk.gitlab.io": customDomain(projectConfig{
24		projectID:  123,
25		pathOnDisk: "@hashed/zip-from-disk.gitlab.io",
26	}),
27	"zip-from-disk-not-found.gitlab.io": customDomain(projectConfig{}),
28	// outside of working dir
29	"zip-not-allowed-path.gitlab.io":  customDomain(projectConfig{pathOnDisk: "../../../../"}),
30	"group.gitlab-example.com":        generateVirtualDomainFromDir("group", "group.gitlab-example.com", nil),
31	"CapitalGroup.gitlab-example.com": generateVirtualDomainFromDir("CapitalGroup", "CapitalGroup.gitlab-example.com", nil),
32	"group.404.gitlab-example.com": generateVirtualDomainFromDir("group.404", "group.404.gitlab-example.com", map[string]projectConfig{
33		"/private_project": {
34			projectID:     1300,
35			accessControl: true,
36		},
37		"/private_unauthorized": {
38			projectID:     2000,
39			accessControl: true,
40		},
41	}),
42	"group.https-only.gitlab-example.com": generateVirtualDomainFromDir("group.https-only", "group.https-only.gitlab-example.com", map[string]projectConfig{
43		"/project1": {
44			projectID: 1000,
45			https:     true,
46		},
47		"/project2": {
48			projectID: 1100,
49			https:     false,
50		},
51	}),
52	"domain.404.com": customDomain(projectConfig{
53		projectID:  1000,
54		pathOnDisk: "group.404/domain.404",
55	}),
56	"withacmechallenge.domain.com": customDomain(projectConfig{
57		projectID:  1234,
58		pathOnDisk: "group.acme/with.acme.challenge",
59	}),
60	"group.redirects.gitlab-example.com": generateVirtualDomainFromDir("group.redirects", "group.redirects.gitlab-example.com", nil),
61	"redirects.custom-domain.com": customDomain(projectConfig{
62		projectID:  1001,
63		pathOnDisk: "group.redirects/custom-domain",
64	}),
65	"test.my-domain.com": customDomain(projectConfig{
66		projectID:  1002,
67		https:      true,
68		pathOnDisk: "group.https-only/project3",
69	}),
70	"test2.my-domain.com": customDomain(projectConfig{
71		projectID:  1003,
72		https:      false,
73		pathOnDisk: "group.https-only/project4",
74	}),
75	"no.cert.com": customDomain(projectConfig{
76		projectID:  1004,
77		https:      true,
78		pathOnDisk: "group.https-only/project5",
79	}),
80	"group.auth.gitlab-example.com": generateVirtualDomainFromDir("group.auth", "group.auth.gitlab-example.com", map[string]projectConfig{
81		"/": {
82			projectID:     1005,
83			accessControl: true,
84		},
85		"/private.project": {
86			projectID:     1006,
87			accessControl: true,
88		},
89		"/private.project.1": {
90			projectID:     2006,
91			accessControl: true,
92		},
93		"/private.project.2": {
94			projectID:     3006,
95			accessControl: true,
96		},
97		"/subgroup/private.project": {
98			projectID:     1007,
99			accessControl: true,
100		},
101		"/subgroup/private.project.1": {
102			projectID:     2007,
103			accessControl: true,
104		},
105		"/subgroup/private.project.2": {
106			projectID:     3007,
107			accessControl: true,
108		},
109	}),
110	"private.domain.com": customDomain(projectConfig{
111		projectID:     1007,
112		accessControl: true,
113		pathOnDisk:    "group.auth/private.project",
114	}),
115	// NOTE: before adding more domains here, generate the zip archive by running (per project)
116	// make zip PROJECT_SUBDIR=group/serving
117	// make zip PROJECT_SUBDIR=group/project2
118}
119
120// generateVirtualDomainFromDir walks the subdirectory inside of shared/pages/ to find any zip archives.
121// It works for subdomains of pages-domain but not for custom domains (yet)
122func generateVirtualDomainFromDir(dir, rootDomain string, perPrefixConfig map[string]projectConfig) responseFn {
123	return func(t *testing.T, wd string) api.VirtualDomain {
124		t.Helper()
125
126		var foundZips []string
127
128		// walk over dir and save any paths containing a `.zip` file
129		// $(GITLAB_PAGES_DIR)/shared/pages + "/" + group
130
131		cleanDir := filepath.Join(wd, dir)
132
133		// make sure resolved path inside dir is under wd to avoid https://securego.io/docs/rules/g304.html
134		require.Truef(t, strings.HasPrefix(cleanDir, wd), "path %q outside of wd %q", cleanDir, wd)
135
136		filepath.Walk(cleanDir, func(path string, info os.FileInfo, err error) error {
137			require.NoError(t, err)
138
139			if strings.HasSuffix(info.Name(), ".zip") {
140				project := strings.TrimPrefix(path, wd+"/"+dir)
141				foundZips = append(foundZips, project)
142			}
143
144			return nil
145		})
146
147		lookupPaths := make([]api.LookupPath, 0, len(foundZips))
148		// generate lookup paths
149		for _, project := range foundZips {
150			// if project = "group/subgroup/project/public.zip
151			// trim prefix group and suffix /public.zip
152			// so prefix = "/subgroup/project"
153			prefix := strings.TrimPrefix(project, dir)
154			prefix = strings.TrimSuffix(prefix, "/"+filepath.Base(project))
155
156			// use / as prefix when the current prefix matches the rootDomain, e.g.
157			// if request is group.gitlab-example.com/ and group/group.gitlab-example.com/public.zip exists
158			if prefix == "/"+rootDomain {
159				prefix = "/"
160			}
161
162			cfg, ok := perPrefixConfig[prefix]
163			if !ok {
164				cfg = projectConfig{}
165			}
166
167			sourcePath := fmt.Sprintf("file://%s", wd+"/"+dir+project)
168			sum := sha256.Sum256([]byte(sourcePath))
169			sha := hex.EncodeToString(sum[:])
170
171			lookupPath := api.LookupPath{
172				ProjectID:     cfg.projectID,
173				AccessControl: cfg.accessControl,
174				HTTPSOnly:     cfg.https,
175				// gitlab.Resolve logic expects prefix to have ending slash
176				Prefix: ensureEndingSlash(prefix),
177				Source: api.Source{
178					Type:   "zip",
179					Path:   sourcePath,
180					SHA256: sha,
181				},
182			}
183
184			lookupPaths = append(lookupPaths, lookupPath)
185		}
186
187		return api.VirtualDomain{
188			LookupPaths: lookupPaths,
189		}
190	}
191}
192
193type projectConfig struct {
194	// refer to makeGitLabPagesAccessStub for custom HTTP responses per projectID
195	projectID     int
196	accessControl bool
197	https         bool
198	pathOnDisk    string
199}
200
201// customDomain with per project config
202func customDomain(config projectConfig) responseFn {
203	return func(t *testing.T, wd string) api.VirtualDomain {
204		t.Helper()
205
206		sourcePath := fmt.Sprintf("file://%s/%s/public.zip", wd, config.pathOnDisk)
207		sum := sha256.Sum256([]byte(sourcePath))
208		sha := hex.EncodeToString(sum[:])
209
210		return api.VirtualDomain{
211			Certificate: "",
212			Key:         "",
213			LookupPaths: []api.LookupPath{
214				{
215					ProjectID:     config.projectID,
216					AccessControl: config.accessControl,
217					HTTPSOnly:     config.https,
218					// prefix should always be `/` for custom domains, otherwise `resolvePath` will try
219					// to look for files under public/prefix/ when serving content instead of just public/
220					// see internal/serving/disk/ for details
221					Prefix: "/",
222					Source: api.Source{
223						Type:   "zip",
224						SHA256: sha,
225						Path:   sourcePath,
226					},
227				},
228			},
229		}
230	}
231}
232
233func ensureEndingSlash(path string) string {
234	if strings.HasSuffix(path, "/") {
235		return path
236	}
237
238	return path + "/"
239}
240