1package domain
2
3import (
4	"context"
5	"crypto/tls"
6	"errors"
7	"net/http"
8	"sync"
9
10	"gitlab.com/gitlab-org/labkit/errortracking"
11
12	"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
13	"gitlab.com/gitlab-org/gitlab-pages/internal/serving"
14)
15
16// ErrDomainDoesNotExist returned when a domain is not found or when a lookup path
17// for a domain could not be resolved
18var ErrDomainDoesNotExist = errors.New("domain does not exist")
19
20// Domain is a domain that gitlab-pages can serve.
21type Domain struct {
22	Name            string
23	CertificateCert string
24	CertificateKey  string
25
26	Resolver Resolver
27
28	certificate      *tls.Certificate
29	certificateError error
30	certificateOnce  sync.Once
31}
32
33// New creates a new domain with a resolver and existing certificates
34func New(name, cert, key string, resolver Resolver) *Domain {
35	return &Domain{
36		Name:            name,
37		CertificateCert: cert,
38		CertificateKey:  key,
39		Resolver:        resolver,
40	}
41}
42
43// String implements Stringer.
44func (d *Domain) String() string {
45	return d.Name
46}
47
48func (d *Domain) resolve(r *http.Request) (*serving.Request, error) {
49	if d == nil {
50		return nil, ErrDomainDoesNotExist
51	}
52
53	return d.Resolver.Resolve(r)
54}
55
56// GetLookupPath returns a project details based on the request. It returns nil
57// if project does not exist.
58func (d *Domain) GetLookupPath(r *http.Request) (*serving.LookupPath, error) {
59	servingReq, err := d.resolve(r)
60	if err != nil {
61		return nil, err
62	}
63
64	return servingReq.LookupPath, nil
65}
66
67// IsHTTPSOnly figures out if the request should be handled with HTTPS
68// only by looking at group and project level config.
69func (d *Domain) IsHTTPSOnly(r *http.Request) bool {
70	if lookupPath, _ := d.GetLookupPath(r); lookupPath != nil {
71		return lookupPath.IsHTTPSOnly
72	}
73
74	return false
75}
76
77// IsAccessControlEnabled figures out if the request is to a project that has access control enabled
78func (d *Domain) IsAccessControlEnabled(r *http.Request) bool {
79	if lookupPath, _ := d.GetLookupPath(r); lookupPath != nil {
80		return lookupPath.HasAccessControl
81	}
82
83	return false
84}
85
86// IsNamespaceProject figures out if the request is to a namespace project
87func (d *Domain) IsNamespaceProject(r *http.Request) bool {
88	if lookupPath, _ := d.GetLookupPath(r); lookupPath != nil {
89		return lookupPath.IsNamespaceProject
90	}
91
92	return false
93}
94
95// GetProjectID figures out what is the ID of the project user tries to access
96func (d *Domain) GetProjectID(r *http.Request) uint64 {
97	if lookupPath, _ := d.GetLookupPath(r); lookupPath != nil {
98		return lookupPath.ProjectID
99	}
100
101	return 0
102}
103
104// EnsureCertificate parses the PEM-encoded certificate for the domain
105func (d *Domain) EnsureCertificate() (*tls.Certificate, error) {
106	if d == nil || len(d.CertificateKey) == 0 || len(d.CertificateCert) == 0 {
107		return nil, errors.New("tls certificates can be loaded only for pages with configuration")
108	}
109
110	d.certificateOnce.Do(func() {
111		var cert tls.Certificate
112		cert, d.certificateError = tls.X509KeyPair(
113			[]byte(d.CertificateCert),
114			[]byte(d.CertificateKey),
115		)
116		if d.certificateError == nil {
117			d.certificate = &cert
118		}
119	})
120
121	return d.certificate, d.certificateError
122}
123
124// ServeFileHTTP returns true if something was served, false if not.
125func (d *Domain) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool {
126	request, err := d.resolve(r)
127	if err != nil {
128		if errors.Is(err, ErrDomainDoesNotExist) {
129			// serve generic 404
130			httperrors.Serve404(w)
131			return true
132		}
133
134		errortracking.Capture(err, errortracking.WithRequest(r), errortracking.WithStackTrace())
135		httperrors.Serve503(w)
136		return true
137	}
138
139	return request.ServeFileHTTP(w, r)
140}
141
142// ServeNotFoundHTTP serves the not found pages from the projects.
143func (d *Domain) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) {
144	request, err := d.resolve(r)
145	if err != nil {
146		if errors.Is(err, ErrDomainDoesNotExist) {
147			// serve generic 404
148			httperrors.Serve404(w)
149			return
150		}
151
152		errortracking.Capture(err, errortracking.WithRequest(r), errortracking.WithStackTrace())
153		httperrors.Serve503(w)
154		return
155	}
156
157	request.ServeNotFoundHTTP(w, r)
158}
159
160// serveNamespaceNotFound will try to find a parent namespace domain for a request
161// that failed authentication so that we serve the custom namespace error page for
162// public namespace domains
163func (d *Domain) serveNamespaceNotFound(w http.ResponseWriter, r *http.Request) {
164	// clone r and override the path and try to resolve the domain name
165	clonedReq := r.Clone(context.Background())
166	clonedReq.URL.Path = "/"
167
168	namespaceDomain, err := d.Resolver.Resolve(clonedReq)
169	if err != nil {
170		if errors.Is(err, ErrDomainDoesNotExist) {
171			// serve generic 404
172			httperrors.Serve404(w)
173			return
174		}
175
176		errortracking.Capture(err, errortracking.WithRequest(r), errortracking.WithStackTrace())
177		httperrors.Serve503(w)
178		return
179	}
180
181	// for namespace domains that have no access control enabled
182	if !namespaceDomain.LookupPath.HasAccessControl {
183		namespaceDomain.ServeNotFoundHTTP(w, r)
184		return
185	}
186
187	httperrors.Serve404(w)
188}
189
190// ServeNotFoundAuthFailed handler to be called when auth failed so the correct custom
191// 404 page is served.
192func (d *Domain) ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) {
193	lookupPath, err := d.GetLookupPath(r)
194	if err != nil {
195		httperrors.Serve404(w)
196		return
197	}
198
199	if d.IsNamespaceProject(r) && !lookupPath.HasAccessControl {
200		d.ServeNotFoundHTTP(w, r)
201		return
202	}
203
204	d.serveNamespaceNotFound(w, r)
205}
206