github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/server.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libpages 6 7 import ( 8 "context" 9 "crypto/ecdsa" 10 "crypto/elliptic" 11 "crypto/rand" 12 "fmt" 13 "io" 14 "log" 15 "net/http" 16 "os" 17 "path" 18 "reflect" 19 "strings" 20 "sync" 21 "time" 22 23 lru "github.com/hashicorp/golang-lru" 24 "github.com/keybase/client/go/kbfs/libfs" 25 "github.com/keybase/client/go/kbfs/libkbfs" 26 "github.com/keybase/client/go/kbfs/libmime" 27 "github.com/keybase/client/go/kbfs/libpages/config" 28 "github.com/keybase/client/go/kbfs/tlf" 29 "go.uber.org/zap" 30 "golang.org/x/crypto/acme" 31 "golang.org/x/crypto/acme/autocert" 32 ) 33 34 // CertStoreType is a type for specifying if and what cert store should be used 35 // for acme/autocert. 36 type CertStoreType string 37 38 // Possible cert store types. 39 const ( 40 NoCertStore CertStoreType = "" 41 DiskCertStore CertStoreType = "disk" 42 KVStoreCertStore CertStoreType = "kvstore" 43 ) 44 45 // ServerConfig holds configuration parameters for Server. 46 type ServerConfig struct { 47 // If DomainWhitelist is non-nil and non-empty, only domains in the 48 // whitelist are served and others are blocked. 49 DomainWhitelist []string 50 // If DomainBlacklist is non-nil and non-empty, domains in the blacklist 51 // and all subdomains under them are blocked. When a domain is present in 52 // both blacklist and whitelist, the domain is blocked. 53 DomainBlacklist []string 54 UseStaging bool 55 Logger *zap.Logger 56 CertStore CertStoreType 57 StatsReporter StatsReporter 58 59 domainListsOnce sync.Once 60 domainWhitelist map[string]bool 61 domainBlacklist []string 62 } 63 64 // ErrDomainBlockedInBlacklist is returned when the server is configured 65 // with a domain blacklist, and we receive a HTTP request that was sent to a 66 // domain that's in the blacklist. 67 type ErrDomainBlockedInBlacklist struct{} 68 69 // Error implements the error interface. 70 func (ErrDomainBlockedInBlacklist) Error() string { 71 return "a blacklist is configured and the given domain is in the list" 72 } 73 74 // ErrDomainNotAllowedInWhitelist is returned when the server is configured 75 // with a domain whitelist, and we receive a HTTP request that was sent to a 76 // domain that's not in the whitelist. 77 type ErrDomainNotAllowedInWhitelist struct{} 78 79 // Error implements the error interface. 80 func (ErrDomainNotAllowedInWhitelist) Error() string { 81 return "a whitelist is configured and the given domain is not in the list" 82 } 83 84 func (c *ServerConfig) checkDomainLists(domain string) error { 85 c.domainListsOnce.Do(func() { 86 if len(c.DomainWhitelist) > 0 { 87 c.domainWhitelist = make(map[string]bool, len(c.DomainWhitelist)) 88 for _, d := range c.DomainWhitelist { 89 c.domainWhitelist[strings.ToLower(strings.TrimSpace(d))] = true 90 } 91 } 92 if len(c.DomainBlacklist) > 0 { 93 c.domainBlacklist = make([]string, len(c.DomainBlacklist)) 94 for i, d := range c.DomainBlacklist { 95 c.domainBlacklist[i] = strings.ToLower(strings.TrimSpace(d)) 96 } 97 } 98 }) 99 100 for _, blocked := range c.domainBlacklist { 101 if strings.HasSuffix(domain, blocked) { 102 return ErrDomainBlockedInBlacklist{} 103 } 104 } 105 if len(c.domainWhitelist) > 0 && !c.domainWhitelist[domain] { 106 return ErrDomainNotAllowedInWhitelist{} 107 } 108 109 // No domainWhitelist; allow everything! 110 return nil 111 } 112 113 const fsCacheSize = 2 << 15 114 115 // Server handles incoming HTTP requests by creating a Root for each host and 116 // serving content from it. 117 type Server struct { 118 config *ServerConfig 119 kbfsConfig libkbfs.Config 120 121 rootLoader RootLoader 122 siteCache *lru.Cache 123 } 124 125 func (s *Server) getSite(ctx context.Context, root Root) (st *site, err error) { 126 siteCached, ok := s.siteCache.Get(root) 127 if ok { 128 if st, ok := siteCached.(*site); ok { 129 if !st.fs.IsObsolete() { 130 return st, nil 131 } 132 s.config.Logger.Info("fs end of life", 133 zap.String("root", fmt.Sprintf("%#+v", root))) 134 } 135 s.config.Logger.Error("nasty entry in s.siteCache", 136 zap.String("reflect_type", reflect.TypeOf(siteCached).String())) 137 } 138 fs, tlfID, fsShutdown, err := root.MakeFS(ctx, s.config.Logger, s.kbfsConfig) 139 if err != nil { 140 return nil, err 141 } 142 var added bool 143 defer func() { 144 // This is in case there's a panic before we get to add st into 145 // s.siteCache. 146 if !added { 147 fsShutdown() 148 } 149 }() 150 st = makeSite(fs, tlfID, fsShutdown, root) 151 s.siteCache.Add(root, st) 152 added = true 153 return st, nil 154 } 155 156 func (s *Server) siteCacheEvict(_ interface{}, value interface{}) { 157 if s, ok := value.(*site); ok { 158 // It's possible to have a race here where a site gets evicted by the 159 // LRU cache while the server is still using it to serve a request. But 160 // since the cache is LRU, this should almost never happen given a 161 // sufficiently large cache, and under the assumption that serving a 162 // request won't take super long. 163 s.shutdown() 164 return 165 } 166 s.config.Logger.Error("nasty entry in s.siteCache", 167 zap.String("reflect_type", reflect.TypeOf(value).String())) 168 } 169 170 func (s *Server) handleError(w http.ResponseWriter, err error) { 171 // TODO: have a nicer error page for configuration errors? 172 switch err.(type) { 173 case nil: 174 case ErrKeybasePagesRecordNotFound, 175 ErrDomainNotAllowedInWhitelist, ErrDomainBlockedInBlacklist: 176 http.Error(w, err.Error(), http.StatusServiceUnavailable) 177 return 178 case ErrKeybasePagesRecordTooMany, ErrInvalidKeybasePagesRecord: 179 http.Error(w, err.Error(), http.StatusPreconditionFailed) 180 return 181 case config.ErrDuplicatePerPathConfigPath, config.ErrInvalidPermissions, 182 config.ErrInvalidVersion, config.ErrUndefinedUsername: 183 http.Error(w, "invalid .kbp_config", http.StatusPreconditionFailed) 184 return 185 default: 186 // Don't write unknown errors in case we leak data unintentionally. 187 http.Error(w, "", http.StatusInternalServerError) 188 return 189 } 190 } 191 192 // CtxKBPTagKey is the type used for unique context tags within kbp and 193 // libpages. 194 type CtxKBPTagKey int 195 196 const ( 197 // CtxKBPKey is the tag key for unique operation IDs within kbp and 198 // libpages. 199 CtxKBPKey CtxKBPTagKey = iota 200 ) 201 202 // CtxKBPOpID is the display name for unique operations in kbp and libpages. 203 const CtxKBPOpID = "KBP" 204 205 type adaptedLogger struct { 206 msg string 207 logger *zap.Logger 208 } 209 210 func (a adaptedLogger) Warning(format string, args ...interface{}) { 211 a.logger.Warn(a.msg, zap.String("desc", fmt.Sprintf(format, args...))) 212 } 213 214 func (s *Server) handleUnauthorized(w http.ResponseWriter, 215 r *http.Request, realm string, authorizationPossible bool) { 216 if authorizationPossible { 217 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", realm)) 218 w.WriteHeader(http.StatusUnauthorized) 219 } else { 220 w.WriteHeader(http.StatusForbidden) 221 } 222 } 223 224 func (s *Server) isDirWithNoIndexHTML( 225 realFS *libfs.FS, requestPath string) (bool, error) { 226 fi, err := realFS.Stat(strings.Trim(path.Clean(requestPath), "/")) 227 switch { 228 case os.IsNotExist(err): 229 // It doesn't exist! So just let the http package handle it. 230 return false, nil 231 case err != nil: 232 // Some other error happened. To be safe, error here. 233 return false, err 234 default: 235 // continue 236 } 237 238 if !fi.IsDir() { 239 return false, nil 240 } 241 242 _, err = realFS.Stat(path.Join(requestPath, "index.html")) 243 switch { 244 case err == nil: 245 return false, nil 246 case os.IsNotExist(err): 247 return true, nil 248 default: 249 // Some other error happened. To be safe, error here. 250 return false, err 251 } 252 } 253 254 // ServedRequestInfo holds information regarding to an incoming request 255 // that might be useful for stats. 256 type ServedRequestInfo struct { 257 // Host is the `Host` field of http.Request. 258 Host string 259 // Proto is the `Proto` field of http.Request. 260 Proto string 261 // Authenticated means the client set WWW-Authenticate in this request and 262 // authentication using the given credentials has succeeded. It doesn't 263 // necessarily indicate that the authentication is required for this 264 // particular request. 265 Authenticated bool 266 // TlfID is the TLF ID associated with the site. 267 TlfID tlf.ID 268 // TlfType is the TLF type of the root that's used to serve the request. 269 TlfType tlf.Type 270 // RootType is the type of the root that's used to serve the request. 271 RootType RootType 272 // HTTPStatus is the HTTP status code that we have written for the request 273 // in the response header. 274 HTTPStatus int 275 // CloningShown is set to true if a "CLONING" page instead of the real site 276 // was served to the request. 277 CloningShown bool 278 // InvalidConfig is set to true if user has a config for the site being 279 // requested, but it's invalid. 280 InvalidConfig bool 281 } 282 283 type statusCodePeekingResponseWriter struct { 284 w http.ResponseWriter 285 code *int 286 } 287 288 var _ http.ResponseWriter = statusCodePeekingResponseWriter{} 289 290 func (w statusCodePeekingResponseWriter) Header() http.Header { 291 return w.w.Header() 292 } 293 294 func (w statusCodePeekingResponseWriter) WriteHeader(status int) { 295 if *w.code == 0 { 296 *w.code = status 297 } 298 w.w.WriteHeader(status) 299 } 300 301 func (w statusCodePeekingResponseWriter) Write(data []byte) (int, error) { 302 if *w.code == 0 { 303 *w.code = http.StatusOK 304 } 305 return w.w.Write(data) 306 } 307 308 func (s *ServedRequestInfo) wrapResponseWriter( 309 w http.ResponseWriter) http.ResponseWriter { 310 return statusCodePeekingResponseWriter{w: w, code: &s.HTTPStatus} 311 } 312 313 func (s *Server) logRequest(sri *ServedRequestInfo, requestPath string, startTime time.Time, err *error) { 314 s.config.Logger.Info("ReqProcessed", 315 zap.String("host", sri.Host), 316 zap.String("path", requestPath), 317 zap.String("proto", sri.Proto), 318 zap.String("tlf_id", sri.TlfID.String()), 319 zap.Int("http_status", sri.HTTPStatus), 320 zap.Bool("authenticated", sri.Authenticated), 321 zap.Bool("cloning_shown", sri.CloningShown), 322 zap.Bool("invalid_config", sri.InvalidConfig), 323 zap.Duration("duration", time.Since(startTime)), 324 zap.NamedError("pre_FileServer_error", *err), 325 ) 326 } 327 328 func (s *Server) setCommonResponseHeaders(w http.ResponseWriter) { 329 // Enforce XSS protection. References: 330 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 331 // https://blog.innerht.ml/the-misunderstood-x-xss-protection/ 332 w.Header().Set("X-XSS-Protection", "1; mode=block") 333 // Only allow HTTPS on this domain, and make this policy expire in a 334 // week. This means if user decides to migrate off Keybase Pages, there's a 335 // 1-week gap before they can use HTTP again. Note that we don't use the 336 // 'preload' directive, for the same reason we use 302 instead of 301 for 337 // HTTP->HTTPS redirection. Reference: https://hstspreload.org/#opt-in 338 w.Header().Set("Strict-Transport-Security", "max-age=604800") 339 // TODO: allow user to opt-in some directives of Content-Security-Policy? 340 } 341 342 func (s *Server) setAccessControlAllowOriginHeader(w http.ResponseWriter, accessControlAllowOrigin string) { 343 w.Header().Set("Access-Control-Allow-Origin", accessControlAllowOrigin) 344 } 345 346 // ServeHTTP implements the http.Handler interface. 347 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 348 startTime := time.Now() 349 sri := &ServedRequestInfo{ 350 Proto: r.Proto, 351 Host: r.Host, 352 } 353 w = sri.wrapResponseWriter(w) 354 if s.config.StatsReporter != nil { 355 defer s.config.StatsReporter.ReportServedRequest(sri) 356 } 357 358 var err error 359 defer s.logRequest(sri, r.URL.Path, startTime, &err) 360 361 if err = s.config.checkDomainLists(r.Host); err != nil { 362 s.handleError(w, err) 363 return 364 } 365 366 s.setCommonResponseHeaders(w) 367 368 // Don't serve the config file itself. 369 if path.Clean(strings.ToLower(r.URL.Path)) == config.DefaultConfigFilepath { 370 // TODO: integrate this check into Config? 371 http.Error(w, fmt.Sprintf("Reading %s directly is forbidden.", 372 config.DefaultConfigFilepath), http.StatusForbidden) 373 return 374 } 375 376 // Construct a *site from DNS record. 377 root, err := s.rootLoader.LoadRoot(r.Host) 378 if err != nil { 379 s.handleError(w, err) 380 return 381 } 382 sri.TlfType, sri.RootType = root.TlfType, root.Type 383 ctx := libfs.EnableFastMode( 384 libkbfs.CtxWithRandomIDReplayable(r.Context(), 385 CtxKBPKey, CtxKBPOpID, adaptedLogger{ 386 msg: "CtxWithRandomIDReplayable", 387 logger: s.config.Logger, 388 }), 389 ) 390 st, err := s.getSite(ctx, root) 391 if err != nil { 392 s.handleError(w, err) 393 return 394 } 395 sri.TlfID = st.tlfID 396 397 realFS, err := st.fs.Use() 398 if err != nil { 399 s.handleError(w, err) 400 return 401 } 402 403 // Get a site config, which can be either a user-defined one, or the 404 // default one if it's missing from the site root. 405 cfg, err := st.getConfig(false) 406 if err != nil { 407 // User has a .kbp_config file but it's invalid. 408 // TODO: error page to show the error message? 409 sri.InvalidConfig = true 410 s.handleError(w, err) 411 return 412 } 413 414 var username *string 415 user, pass, ok := r.BasicAuth() 416 if ok && cfg.Authenticate(r.Context(), user, pass) { 417 sri.Authenticated = true 418 username = &user 419 } 420 canRead, canList, possibleRead, possibleList, 421 realm, err := cfg.GetPermissions(r.URL.Path, username) 422 if err != nil { 423 s.handleError(w, err) 424 return 425 } 426 427 // Check if it's a directory containing no index.html before letting 428 // http.FileServer handle it. This permission check should ideally 429 // happen inside the http package, but unfortunately there isn't a 430 // way today. 431 isListing, err := s.isDirWithNoIndexHTML(realFS, r.URL.Path) 432 if err != nil { 433 s.handleError(w, err) 434 return 435 } 436 437 if isListing && !canList { 438 s.handleUnauthorized(w, r, realm, possibleList) 439 return 440 } 441 442 if !isListing && !canRead { 443 s.handleUnauthorized(w, r, realm, possibleRead) 444 return 445 } 446 447 accessControlAllowOrigin, err := cfg.GetAccessControlAllowOrigin(r.URL.Path) 448 if err != nil { 449 s.handleError(w, err) 450 return 451 } 452 if len(accessControlAllowOrigin) > 0 { 453 s.setAccessControlAllowOriginHeader(w, accessControlAllowOrigin) 454 } 455 456 http.FileServer(realFS.ToHTTPFileSystem(ctx)).ServeHTTP(w, r) 457 } 458 459 // allowDomain is used to determine whether a given domain should be 460 // served. It's also used as a HostPolicy in autocert package. 461 func (s *Server) allowDomain(ctx context.Context, host string) (err error) { 462 host = strings.ToLower(strings.TrimSpace(host)) 463 if err = s.config.checkDomainLists(host); err != nil { 464 return err 465 } 466 467 // DoS protection: look up kbp TXT record before attempting ACME cert 468 // issuance, and only allow those that have DNS records configured. This is 469 // in case someone keeps sending us TLS handshakes with random SNIs, 470 // causing us to be rate-limited by the ACME server. 471 // 472 // TODO: cache the parsed root somewhere so we don't end up doing it twice 473 // for each connection. 474 if _, err = s.rootLoader.LoadRoot(host); err != nil { 475 return err 476 } 477 478 return nil 479 } 480 481 const ( 482 gracefulShutdownTimeout = 16 * time.Second 483 httpReadHeaderTimeout = 8 * time.Second 484 httpIdleTimeout = 1 * time.Minute 485 stagingDiskCacheName = "./kbp-cert-cache-staging" 486 prodDiskCacheName = "./kbp-cert-cache" 487 ) 488 489 func makeACMEManager(kbfsConfig libkbfs.Config, useStaging bool, 490 certStoreType CertStoreType, hostPolicy autocert.HostPolicy) ( 491 *autocert.Manager, error) { 492 manager := &autocert.Manager{ 493 Prompt: autocert.AcceptTOS, 494 HostPolicy: hostPolicy, 495 } 496 497 switch certStoreType { 498 case DiskCertStore: 499 if useStaging { 500 manager.Cache = autocert.DirCache(stagingDiskCacheName) 501 } else { 502 manager.Cache = autocert.DirCache(prodDiskCacheName) 503 } 504 case KVStoreCertStore: 505 manager.Cache = newCertStoreBackedByKVStore(kbfsConfig) 506 default: 507 } 508 509 if useStaging { 510 acmeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 511 if err != nil { 512 return nil, err 513 } 514 manager.Client = &acme.Client{ 515 DirectoryURL: "https://acme-staging.api.letsencrypt.org/directory", 516 Key: acmeKey, 517 } 518 } 519 520 return manager, nil 521 } 522 523 var additionalMimeTypes = map[string]string{ 524 ".wasm": "application/wasm", 525 } 526 527 type logForwarder struct { 528 msg string 529 logFunc func(msg string, fields ...zap.Field) 530 } 531 532 var _ io.Writer = (*logForwarder)(nil) 533 534 func (f *logForwarder) Write(p []byte) (n int, err error) { 535 f.logFunc(f.msg, zap.String("content", string(p))) 536 return len(p), nil 537 } 538 539 // ListenAndServe listens on 443 and 80 ports of all addresses, and serve 540 // Keybase Pages based on config and kbfsConfig. HTTPs setup is handled with 541 // ACME. 542 func ListenAndServe(ctx context.Context, 543 config *ServerConfig, kbfsConfig libkbfs.Config) (err error) { 544 ctx, cancel := context.WithCancel(ctx) 545 defer cancel() 546 547 libmime.Patch(additionalMimeTypes) 548 549 server := &Server{ 550 config: config, 551 kbfsConfig: kbfsConfig, 552 rootLoader: NewDNSRootLoader(config.Logger), 553 } 554 server.siteCache, err = lru.NewWithEvict(fsCacheSize, server.siteCacheEvict) 555 if err != nil { 556 return err 557 } 558 559 manager, err := makeACMEManager( 560 kbfsConfig, config.UseStaging, config.CertStore, server.allowDomain) 561 if err != nil { 562 return err 563 } 564 565 if manager.Client == nil || len(manager.Client.DirectoryURL) == 0 { 566 config.Logger.Info("ListenAndServe", 567 zap.String("acme directory url", autocert.DefaultACMEDirectory)) 568 } else { 569 config.Logger.Info("ListenAndServe", 570 zap.String("acme directory url", manager.Client.DirectoryURL)) 571 } 572 573 httpsServer := http.Server{ 574 Handler: server, 575 ReadHeaderTimeout: httpReadHeaderTimeout, 576 IdleTimeout: httpIdleTimeout, 577 ErrorLog: log.New(&logForwarder{ 578 msg: "http error log", 579 logFunc: config.Logger.Error, 580 }, "", 0), 581 } 582 583 httpServer := http.Server{ 584 Addr: ":80", 585 // Enable http-01 by calling the HTTPHandler method, and set the 586 // fallback HTTP handler to nil. As described in the autocert doc 587 // (https://github.com/golang/crypto/blob/13931e22f9e72ea58bb73048bc752b48c6d4d4ac/acme/autocert/autocert.go#L248-L251), 588 // this means for requests not for ACME domain verification, a default 589 // fallback handler is used, which redirects all HTTP traffic using GET 590 // and HEAD to HTTPS using 302 Found, and responds with 400 Bad Request 591 // for requests with other methods. 592 Handler: manager.HTTPHandler(nil), 593 ReadHeaderTimeout: httpReadHeaderTimeout, 594 IdleTimeout: httpIdleTimeout, 595 } 596 597 go func() { 598 <-ctx.Done() 599 shutdownCtx, cancel := context.WithTimeout( 600 context.Background(), gracefulShutdownTimeout) 601 defer cancel() 602 _ = httpsServer.Shutdown(shutdownCtx) 603 _ = httpServer.Shutdown(shutdownCtx) 604 }() 605 606 go func() { 607 err := httpServer.ListenAndServe() 608 if err != nil { 609 config.Logger.Error("http.ListenAndServe:80", zap.Error(err)) 610 } 611 }() 612 613 return httpsServer.Serve(manager.Listener()) 614 }