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