github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/handlers/app.go (about) 1 package handlers 2 3 import ( 4 cryptorand "crypto/rand" 5 "expvar" 6 "fmt" 7 "math/rand" 8 "net" 9 "net/http" 10 "net/url" 11 "os" 12 "time" 13 14 log "github.com/Sirupsen/logrus" 15 "github.com/docker/distribution" 16 "github.com/docker/distribution/configuration" 17 ctxu "github.com/docker/distribution/context" 18 "github.com/docker/distribution/health" 19 "github.com/docker/distribution/health/checks" 20 "github.com/docker/distribution/notifications" 21 "github.com/docker/distribution/registry/api/errcode" 22 "github.com/docker/distribution/registry/api/v2" 23 "github.com/docker/distribution/registry/auth" 24 registrymiddleware "github.com/docker/distribution/registry/middleware/registry" 25 repositorymiddleware "github.com/docker/distribution/registry/middleware/repository" 26 "github.com/docker/distribution/registry/proxy" 27 "github.com/docker/distribution/registry/storage" 28 memorycache "github.com/docker/distribution/registry/storage/cache/memory" 29 rediscache "github.com/docker/distribution/registry/storage/cache/redis" 30 storagedriver "github.com/docker/distribution/registry/storage/driver" 31 "github.com/docker/distribution/registry/storage/driver/factory" 32 storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" 33 "github.com/docker/libtrust" 34 "github.com/garyburd/redigo/redis" 35 "github.com/gorilla/mux" 36 "golang.org/x/net/context" 37 ) 38 39 // randomSecretSize is the number of random bytes to generate if no secret 40 // was specified. 41 const randomSecretSize = 32 42 43 // defaultCheckInterval is the default time in between health checks 44 const defaultCheckInterval = 10 * time.Second 45 46 // App is a global registry application object. Shared resources can be placed 47 // on this object that will be accessible from all requests. Any writable 48 // fields should be protected. 49 type App struct { 50 context.Context 51 52 Config *configuration.Configuration 53 54 router *mux.Router // main application router, configured with dispatchers 55 driver storagedriver.StorageDriver // driver maintains the app global storage driver instance. 56 registry distribution.Namespace // registry is the primary registry backend for the app instance. 57 accessController auth.AccessController // main access controller for application 58 59 // httpHost is a parsed representation of the http.host parameter from 60 // the configuration. Only the Scheme and Host fields are used. 61 httpHost url.URL 62 63 // events contains notification related configuration. 64 events struct { 65 sink notifications.Sink 66 source notifications.SourceRecord 67 } 68 69 redis *redis.Pool 70 71 // trustKey is a deprecated key used to sign manifests converted to 72 // schema1 for backward compatibility. It should not be used for any 73 // other purposes. 74 trustKey libtrust.PrivateKey 75 76 // isCache is true if this registry is configured as a pull through cache 77 isCache bool 78 79 // readOnly is true if the registry is in a read-only maintenance mode 80 readOnly bool 81 } 82 83 // NewApp takes a configuration and returns a configured app, ready to serve 84 // requests. The app only implements ServeHTTP and can be wrapped in other 85 // handlers accordingly. 86 func NewApp(ctx context.Context, configuration *configuration.Configuration) *App { 87 app := &App{ 88 Config: configuration, 89 Context: ctx, 90 router: v2.RouterWithPrefix(configuration.HTTP.Prefix), 91 isCache: configuration.Proxy.RemoteURL != "", 92 } 93 94 // Register the handler dispatchers. 95 app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { 96 return http.HandlerFunc(apiBase) 97 }) 98 app.register(v2.RouteNameManifest, imageManifestDispatcher) 99 app.register(v2.RouteNameCatalog, catalogDispatcher) 100 app.register(v2.RouteNameTags, tagsDispatcher) 101 app.register(v2.RouteNameBlob, blobDispatcher) 102 app.register(v2.RouteNameBlobUpload, blobUploadDispatcher) 103 app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher) 104 105 var err error 106 app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters()) 107 if err != nil { 108 // TODO(stevvooe): Move the creation of a service into a protected 109 // method, where this is created lazily. Its status can be queried via 110 // a health check. 111 panic(err) 112 } 113 114 purgeConfig := uploadPurgeDefaultConfig() 115 if mc, ok := configuration.Storage["maintenance"]; ok { 116 if v, ok := mc["uploadpurging"]; ok { 117 purgeConfig, ok = v.(map[interface{}]interface{}) 118 if !ok { 119 panic("uploadpurging config key must contain additional keys") 120 } 121 } 122 if v, ok := mc["readonly"]; ok { 123 readOnly, ok := v.(map[interface{}]interface{}) 124 if !ok { 125 panic("readonly config key must contain additional keys") 126 } 127 if readOnlyEnabled, ok := readOnly["enabled"]; ok { 128 app.readOnly, ok = readOnlyEnabled.(bool) 129 if !ok { 130 panic("readonly's enabled config key must have a boolean value") 131 } 132 } 133 } 134 } 135 136 startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig) 137 138 app.driver, err = applyStorageMiddleware(app.driver, configuration.Middleware["storage"]) 139 if err != nil { 140 panic(err) 141 } 142 143 app.configureSecret(configuration) 144 app.configureEvents(configuration) 145 app.configureRedis(configuration) 146 app.configureLogHook(configuration) 147 148 // Generate an ephemeral key to be used for signing converted manifests 149 // for clients that don't support schema2. 150 app.trustKey, err = libtrust.GenerateECP256PrivateKey() 151 if err != nil { 152 panic(err) 153 } 154 155 if configuration.HTTP.Host != "" { 156 u, err := url.Parse(configuration.HTTP.Host) 157 if err != nil { 158 panic(fmt.Sprintf(`could not parse http "host" parameter: %v`, err)) 159 } 160 app.httpHost = *u 161 } 162 163 options := []storage.RegistryOption{} 164 165 if app.isCache { 166 options = append(options, storage.DisableDigestResumption) 167 } 168 169 // configure deletion 170 if d, ok := configuration.Storage["delete"]; ok { 171 e, ok := d["enabled"] 172 if ok { 173 if deleteEnabled, ok := e.(bool); ok && deleteEnabled { 174 options = append(options, storage.EnableDelete) 175 } 176 } 177 } 178 179 // configure redirects 180 var redirectDisabled bool 181 if redirectConfig, ok := configuration.Storage["redirect"]; ok { 182 v := redirectConfig["disable"] 183 switch v := v.(type) { 184 case bool: 185 redirectDisabled = v 186 default: 187 panic(fmt.Sprintf("invalid type for redirect config: %#v", redirectConfig)) 188 } 189 } 190 if redirectDisabled { 191 ctxu.GetLogger(app).Infof("backend redirection disabled") 192 } else { 193 options = append(options, storage.EnableRedirect) 194 } 195 196 // configure storage caches 197 if cc, ok := configuration.Storage["cache"]; ok { 198 v, ok := cc["blobdescriptor"] 199 if !ok { 200 // Backwards compatible: "layerinfo" == "blobdescriptor" 201 v = cc["layerinfo"] 202 } 203 204 switch v { 205 case "redis": 206 if app.redis == nil { 207 panic("redis configuration required to use for layerinfo cache") 208 } 209 cacheProvider := rediscache.NewRedisBlobDescriptorCacheProvider(app.redis) 210 localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider)) 211 app.registry, err = storage.NewRegistry(app, app.driver, localOptions...) 212 if err != nil { 213 panic("could not create registry: " + err.Error()) 214 } 215 ctxu.GetLogger(app).Infof("using redis blob descriptor cache") 216 case "inmemory": 217 cacheProvider := memorycache.NewInMemoryBlobDescriptorCacheProvider() 218 localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider)) 219 app.registry, err = storage.NewRegistry(app, app.driver, localOptions...) 220 if err != nil { 221 panic("could not create registry: " + err.Error()) 222 } 223 ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache") 224 default: 225 if v != "" { 226 ctxu.GetLogger(app).Warnf("unknown cache type %q, caching disabled", configuration.Storage["cache"]) 227 } 228 } 229 } 230 231 if app.registry == nil { 232 // configure the registry if no cache section is available. 233 app.registry, err = storage.NewRegistry(app.Context, app.driver, options...) 234 if err != nil { 235 panic("could not create registry: " + err.Error()) 236 } 237 } 238 239 app.registry, err = applyRegistryMiddleware(app.Context, app.registry, configuration.Middleware["registry"]) 240 if err != nil { 241 panic(err) 242 } 243 244 authType := configuration.Auth.Type() 245 246 if authType != "" { 247 accessController, err := auth.GetAccessController(configuration.Auth.Type(), configuration.Auth.Parameters()) 248 if err != nil { 249 panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) 250 } 251 app.accessController = accessController 252 ctxu.GetLogger(app).Debugf("configured %q access controller", authType) 253 } 254 255 // configure as a pull through cache 256 if configuration.Proxy.RemoteURL != "" { 257 app.registry, err = proxy.NewRegistryPullThroughCache(ctx, app.registry, app.driver, configuration.Proxy) 258 if err != nil { 259 panic(err.Error()) 260 } 261 app.isCache = true 262 ctxu.GetLogger(app).Info("Registry configured as a proxy cache to ", configuration.Proxy.RemoteURL) 263 } 264 265 return app 266 } 267 268 // RegisterHealthChecks is an awful hack to defer health check registration 269 // control to callers. This should only ever be called once per registry 270 // process, typically in a main function. The correct way would be register 271 // health checks outside of app, since multiple apps may exist in the same 272 // process. Because the configuration and app are tightly coupled, 273 // implementing this properly will require a refactor. This method may panic 274 // if called twice in the same process. 275 func (app *App) RegisterHealthChecks(healthRegistries ...*health.Registry) { 276 if len(healthRegistries) > 1 { 277 panic("RegisterHealthChecks called with more than one registry") 278 } 279 healthRegistry := health.DefaultRegistry 280 if len(healthRegistries) == 1 { 281 healthRegistry = healthRegistries[0] 282 } 283 284 if app.Config.Health.StorageDriver.Enabled { 285 interval := app.Config.Health.StorageDriver.Interval 286 if interval == 0 { 287 interval = defaultCheckInterval 288 } 289 290 storageDriverCheck := func() error { 291 _, err := app.driver.List(app, "/") // "/" should always exist 292 return err // any error will be treated as failure 293 } 294 295 if app.Config.Health.StorageDriver.Threshold != 0 { 296 healthRegistry.RegisterPeriodicThresholdFunc("storagedriver_"+app.Config.Storage.Type(), interval, app.Config.Health.StorageDriver.Threshold, storageDriverCheck) 297 } else { 298 healthRegistry.RegisterPeriodicFunc("storagedriver_"+app.Config.Storage.Type(), interval, storageDriverCheck) 299 } 300 } 301 302 for _, fileChecker := range app.Config.Health.FileCheckers { 303 interval := fileChecker.Interval 304 if interval == 0 { 305 interval = defaultCheckInterval 306 } 307 ctxu.GetLogger(app).Infof("configuring file health check path=%s, interval=%d", fileChecker.File, interval/time.Second) 308 healthRegistry.Register(fileChecker.File, health.PeriodicChecker(checks.FileChecker(fileChecker.File), interval)) 309 } 310 311 for _, httpChecker := range app.Config.Health.HTTPCheckers { 312 interval := httpChecker.Interval 313 if interval == 0 { 314 interval = defaultCheckInterval 315 } 316 317 statusCode := httpChecker.StatusCode 318 if statusCode == 0 { 319 statusCode = 200 320 } 321 322 checker := checks.HTTPChecker(httpChecker.URI, statusCode, httpChecker.Timeout, httpChecker.Headers) 323 324 if httpChecker.Threshold != 0 { 325 ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d, threshold=%d", httpChecker.URI, interval/time.Second, httpChecker.Threshold) 326 healthRegistry.Register(httpChecker.URI, health.PeriodicThresholdChecker(checker, interval, httpChecker.Threshold)) 327 } else { 328 ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d", httpChecker.URI, interval/time.Second) 329 healthRegistry.Register(httpChecker.URI, health.PeriodicChecker(checker, interval)) 330 } 331 } 332 333 for _, tcpChecker := range app.Config.Health.TCPCheckers { 334 interval := tcpChecker.Interval 335 if interval == 0 { 336 interval = defaultCheckInterval 337 } 338 339 checker := checks.TCPChecker(tcpChecker.Addr, tcpChecker.Timeout) 340 341 if tcpChecker.Threshold != 0 { 342 ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d, threshold=%d", tcpChecker.Addr, interval/time.Second, tcpChecker.Threshold) 343 healthRegistry.Register(tcpChecker.Addr, health.PeriodicThresholdChecker(checker, interval, tcpChecker.Threshold)) 344 } else { 345 ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d", tcpChecker.Addr, interval/time.Second) 346 healthRegistry.Register(tcpChecker.Addr, health.PeriodicChecker(checker, interval)) 347 } 348 } 349 } 350 351 // register a handler with the application, by route name. The handler will be 352 // passed through the application filters and context will be constructed at 353 // request time. 354 func (app *App) register(routeName string, dispatch dispatchFunc) { 355 356 // TODO(stevvooe): This odd dispatcher/route registration is by-product of 357 // some limitations in the gorilla/mux router. We are using it to keep 358 // routing consistent between the client and server, but we may want to 359 // replace it with manual routing and structure-based dispatch for better 360 // control over the request execution. 361 362 app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch)) 363 } 364 365 // configureEvents prepares the event sink for action. 366 func (app *App) configureEvents(configuration *configuration.Configuration) { 367 // Configure all of the endpoint sinks. 368 var sinks []notifications.Sink 369 for _, endpoint := range configuration.Notifications.Endpoints { 370 if endpoint.Disabled { 371 ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name) 372 continue 373 } 374 375 ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers) 376 endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{ 377 Timeout: endpoint.Timeout, 378 Threshold: endpoint.Threshold, 379 Backoff: endpoint.Backoff, 380 Headers: endpoint.Headers, 381 }) 382 383 sinks = append(sinks, endpoint) 384 } 385 386 // NOTE(stevvooe): Moving to a new queueing implementation is as easy as 387 // replacing broadcaster with a rabbitmq implementation. It's recommended 388 // that the registry instances also act as the workers to keep deployment 389 // simple. 390 app.events.sink = notifications.NewBroadcaster(sinks...) 391 392 // Populate registry event source 393 hostname, err := os.Hostname() 394 if err != nil { 395 hostname = configuration.HTTP.Addr 396 } else { 397 // try to pick the port off the config 398 _, port, err := net.SplitHostPort(configuration.HTTP.Addr) 399 if err == nil { 400 hostname = net.JoinHostPort(hostname, port) 401 } 402 } 403 404 app.events.source = notifications.SourceRecord{ 405 Addr: hostname, 406 InstanceID: ctxu.GetStringValue(app, "instance.id"), 407 } 408 } 409 410 func (app *App) configureRedis(configuration *configuration.Configuration) { 411 if configuration.Redis.Addr == "" { 412 ctxu.GetLogger(app).Infof("redis not configured") 413 return 414 } 415 416 pool := &redis.Pool{ 417 Dial: func() (redis.Conn, error) { 418 // TODO(stevvooe): Yet another use case for contextual timing. 419 ctx := context.WithValue(app, "redis.connect.startedat", time.Now()) 420 421 done := func(err error) { 422 logger := ctxu.GetLoggerWithField(ctx, "redis.connect.duration", 423 ctxu.Since(ctx, "redis.connect.startedat")) 424 if err != nil { 425 logger.Errorf("redis: error connecting: %v", err) 426 } else { 427 logger.Infof("redis: connect %v", configuration.Redis.Addr) 428 } 429 } 430 431 conn, err := redis.DialTimeout("tcp", 432 configuration.Redis.Addr, 433 configuration.Redis.DialTimeout, 434 configuration.Redis.ReadTimeout, 435 configuration.Redis.WriteTimeout) 436 if err != nil { 437 ctxu.GetLogger(app).Errorf("error connecting to redis instance %s: %v", 438 configuration.Redis.Addr, err) 439 done(err) 440 return nil, err 441 } 442 443 // authorize the connection 444 if configuration.Redis.Password != "" { 445 if _, err = conn.Do("AUTH", configuration.Redis.Password); err != nil { 446 defer conn.Close() 447 done(err) 448 return nil, err 449 } 450 } 451 452 // select the database to use 453 if configuration.Redis.DB != 0 { 454 if _, err = conn.Do("SELECT", configuration.Redis.DB); err != nil { 455 defer conn.Close() 456 done(err) 457 return nil, err 458 } 459 } 460 461 done(nil) 462 return conn, nil 463 }, 464 MaxIdle: configuration.Redis.Pool.MaxIdle, 465 MaxActive: configuration.Redis.Pool.MaxActive, 466 IdleTimeout: configuration.Redis.Pool.IdleTimeout, 467 TestOnBorrow: func(c redis.Conn, t time.Time) error { 468 // TODO(stevvooe): We can probably do something more interesting 469 // here with the health package. 470 _, err := c.Do("PING") 471 return err 472 }, 473 Wait: false, // if a connection is not avialable, proceed without cache. 474 } 475 476 app.redis = pool 477 478 // setup expvar 479 registry := expvar.Get("registry") 480 if registry == nil { 481 registry = expvar.NewMap("registry") 482 } 483 484 registry.(*expvar.Map).Set("redis", expvar.Func(func() interface{} { 485 return map[string]interface{}{ 486 "Config": configuration.Redis, 487 "Active": app.redis.ActiveCount(), 488 } 489 })) 490 } 491 492 // configureLogHook prepares logging hook parameters. 493 func (app *App) configureLogHook(configuration *configuration.Configuration) { 494 entry, ok := ctxu.GetLogger(app).(*log.Entry) 495 if !ok { 496 // somehow, we are not using logrus 497 return 498 } 499 500 logger := entry.Logger 501 502 for _, configHook := range configuration.Log.Hooks { 503 if !configHook.Disabled { 504 switch configHook.Type { 505 case "mail": 506 hook := &logHook{} 507 hook.LevelsParam = configHook.Levels 508 hook.Mail = &mailer{ 509 Addr: configHook.MailOptions.SMTP.Addr, 510 Username: configHook.MailOptions.SMTP.Username, 511 Password: configHook.MailOptions.SMTP.Password, 512 Insecure: configHook.MailOptions.SMTP.Insecure, 513 From: configHook.MailOptions.From, 514 To: configHook.MailOptions.To, 515 } 516 logger.Hooks.Add(hook) 517 default: 518 } 519 } 520 } 521 } 522 523 // configureSecret creates a random secret if a secret wasn't included in the 524 // configuration. 525 func (app *App) configureSecret(configuration *configuration.Configuration) { 526 if configuration.HTTP.Secret == "" { 527 var secretBytes [randomSecretSize]byte 528 if _, err := cryptorand.Read(secretBytes[:]); err != nil { 529 panic(fmt.Sprintf("could not generate random bytes for HTTP secret: %v", err)) 530 } 531 configuration.HTTP.Secret = string(secretBytes[:]) 532 ctxu.GetLogger(app).Warn("No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable.") 533 } 534 } 535 536 func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { 537 defer r.Body.Close() // ensure that request body is always closed. 538 539 // Instantiate an http context here so we can track the error codes 540 // returned by the request router. 541 ctx := defaultContextManager.context(app, w, r) 542 543 defer func() { 544 status, ok := ctx.Value("http.response.status").(int) 545 if ok && status >= 200 && status <= 399 { 546 ctxu.GetResponseLogger(ctx).Infof("response completed") 547 } 548 }() 549 defer defaultContextManager.release(ctx) 550 551 // NOTE(stevvooe): Total hack to get instrumented responsewriter from context. 552 var err error 553 w, err = ctxu.GetResponseWriter(ctx) 554 if err != nil { 555 ctxu.GetLogger(ctx).Warnf("response writer not found in context") 556 } 557 558 // Set a header with the Docker Distribution API Version for all responses. 559 w.Header().Add("Docker-Distribution-API-Version", "registry/2.0") 560 app.router.ServeHTTP(w, r) 561 } 562 563 // dispatchFunc takes a context and request and returns a constructed handler 564 // for the route. The dispatcher will use this to dynamically create request 565 // specific handlers for each endpoint without creating a new router for each 566 // request. 567 type dispatchFunc func(ctx *Context, r *http.Request) http.Handler 568 569 // TODO(stevvooe): dispatchers should probably have some validation error 570 // chain with proper error reporting. 571 572 // dispatcher returns a handler that constructs a request specific context and 573 // handler, using the dispatch factory function. 574 func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { 575 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 576 for headerName, headerValues := range app.Config.HTTP.Headers { 577 for _, value := range headerValues { 578 w.Header().Add(headerName, value) 579 } 580 } 581 582 context := app.context(w, r) 583 584 if err := app.authorized(w, r, context); err != nil { 585 ctxu.GetLogger(context).Warnf("error authorizing context: %v", err) 586 return 587 } 588 589 // Add username to request logging 590 context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, "auth.user.name")) 591 592 if app.nameRequired(r) { 593 repository, err := app.registry.Repository(context, getName(context)) 594 595 if err != nil { 596 ctxu.GetLogger(context).Errorf("error resolving repository: %v", err) 597 598 switch err := err.(type) { 599 case distribution.ErrRepositoryUnknown: 600 context.Errors = append(context.Errors, v2.ErrorCodeNameUnknown.WithDetail(err)) 601 case distribution.ErrRepositoryNameInvalid: 602 context.Errors = append(context.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) 603 } 604 605 if err := errcode.ServeJSON(w, context.Errors); err != nil { 606 ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) 607 } 608 return 609 } 610 611 // assign and decorate the authorized repository with an event bridge. 612 context.Repository = notifications.Listen( 613 repository, 614 app.eventBridge(context, r)) 615 616 context.Repository, err = applyRepoMiddleware(context.Context, context.Repository, app.Config.Middleware["repository"]) 617 if err != nil { 618 ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err) 619 context.Errors = append(context.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 620 621 if err := errcode.ServeJSON(w, context.Errors); err != nil { 622 ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) 623 } 624 return 625 } 626 } 627 628 dispatch(context, r).ServeHTTP(w, r) 629 // Automated error response handling here. Handlers may return their 630 // own errors if they need different behavior (such as range errors 631 // for layer upload). 632 if context.Errors.Len() > 0 { 633 if err := errcode.ServeJSON(w, context.Errors); err != nil { 634 ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) 635 } 636 637 app.logError(context, context.Errors) 638 } 639 }) 640 } 641 642 func (app *App) logError(context context.Context, errors errcode.Errors) { 643 for _, e1 := range errors { 644 var c ctxu.Context 645 646 switch e1.(type) { 647 case errcode.Error: 648 e, _ := e1.(errcode.Error) 649 c = ctxu.WithValue(context, "err.code", e.Code) 650 c = ctxu.WithValue(c, "err.message", e.Code.Message()) 651 c = ctxu.WithValue(c, "err.detail", e.Detail) 652 case errcode.ErrorCode: 653 e, _ := e1.(errcode.ErrorCode) 654 c = ctxu.WithValue(context, "err.code", e) 655 c = ctxu.WithValue(c, "err.message", e.Message()) 656 default: 657 // just normal go 'error' 658 c = ctxu.WithValue(context, "err.code", errcode.ErrorCodeUnknown) 659 c = ctxu.WithValue(c, "err.message", e1.Error()) 660 } 661 662 c = ctxu.WithLogger(c, ctxu.GetLogger(c, 663 "err.code", 664 "err.message", 665 "err.detail")) 666 ctxu.GetResponseLogger(c).Errorf("response completed with error") 667 } 668 } 669 670 // context constructs the context object for the application. This only be 671 // called once per request. 672 func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { 673 ctx := defaultContextManager.context(app, w, r) 674 ctx = ctxu.WithVars(ctx, r) 675 ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, 676 "vars.name", 677 "vars.reference", 678 "vars.digest", 679 "vars.uuid")) 680 681 context := &Context{ 682 App: app, 683 Context: ctx, 684 } 685 686 if app.httpHost.Scheme != "" && app.httpHost.Host != "" { 687 // A "host" item in the configuration takes precedence over 688 // X-Forwarded-Proto and X-Forwarded-Host headers, and the 689 // hostname in the request. 690 context.urlBuilder = v2.NewURLBuilder(&app.httpHost) 691 } else { 692 context.urlBuilder = v2.NewURLBuilderFromRequest(r) 693 } 694 695 return context 696 } 697 698 // authorized checks if the request can proceed with access to the requested 699 // repository. If it succeeds, the context may access the requested 700 // repository. An error will be returned if access is not available. 701 func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error { 702 ctxu.GetLogger(context).Debug("authorizing request") 703 repo := getName(context) 704 705 if app.accessController == nil { 706 return nil // access controller is not enabled. 707 } 708 709 var accessRecords []auth.Access 710 711 if repo != "" { 712 accessRecords = appendAccessRecords(accessRecords, r.Method, repo) 713 } else { 714 // Only allow the name not to be set on the base route. 715 if app.nameRequired(r) { 716 // For this to be properly secured, repo must always be set for a 717 // resource that may make a modification. The only condition under 718 // which name is not set and we still allow access is when the 719 // base route is accessed. This section prevents us from making 720 // that mistake elsewhere in the code, allowing any operation to 721 // proceed. 722 if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized); err != nil { 723 ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) 724 } 725 return fmt.Errorf("forbidden: no repository name") 726 } 727 accessRecords = appendCatalogAccessRecord(accessRecords, r) 728 } 729 730 ctx, err := app.accessController.Authorized(context.Context, accessRecords...) 731 if err != nil { 732 switch err := err.(type) { 733 case auth.Challenge: 734 // Add the appropriate WWW-Auth header 735 err.SetHeaders(w) 736 737 if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(accessRecords)); err != nil { 738 ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) 739 } 740 default: 741 // This condition is a potential security problem either in 742 // the configuration or whatever is backing the access 743 // controller. Just return a bad request with no information 744 // to avoid exposure. The request should not proceed. 745 ctxu.GetLogger(context).Errorf("error checking authorization: %v", err) 746 w.WriteHeader(http.StatusBadRequest) 747 } 748 749 return err 750 } 751 752 // TODO(stevvooe): This pattern needs to be cleaned up a bit. One context 753 // should be replaced by another, rather than replacing the context on a 754 // mutable object. 755 context.Context = ctx 756 return nil 757 } 758 759 // eventBridge returns a bridge for the current request, configured with the 760 // correct actor and source. 761 func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listener { 762 actor := notifications.ActorRecord{ 763 Name: getUserName(ctx, r), 764 } 765 request := notifications.NewRequestRecord(ctxu.GetRequestID(ctx), r) 766 767 return notifications.NewBridge(ctx.urlBuilder, app.events.source, actor, request, app.events.sink) 768 } 769 770 // nameRequired returns true if the route requires a name. 771 func (app *App) nameRequired(r *http.Request) bool { 772 route := mux.CurrentRoute(r) 773 routeName := route.GetName() 774 return route == nil || (routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog) 775 } 776 777 // apiBase implements a simple yes-man for doing overall checks against the 778 // api. This can support auth roundtrips to support docker login. 779 func apiBase(w http.ResponseWriter, r *http.Request) { 780 const emptyJSON = "{}" 781 // Provide a simple /v2/ 200 OK response with empty json response. 782 w.Header().Set("Content-Type", "application/json; charset=utf-8") 783 w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) 784 785 fmt.Fprint(w, emptyJSON) 786 } 787 788 // appendAccessRecords checks the method and adds the appropriate Access records to the records list. 789 func appendAccessRecords(records []auth.Access, method string, repo string) []auth.Access { 790 resource := auth.Resource{ 791 Type: "repository", 792 Name: repo, 793 } 794 795 switch method { 796 case "GET", "HEAD": 797 records = append(records, 798 auth.Access{ 799 Resource: resource, 800 Action: "pull", 801 }) 802 case "POST", "PUT", "PATCH": 803 records = append(records, 804 auth.Access{ 805 Resource: resource, 806 Action: "pull", 807 }, 808 auth.Access{ 809 Resource: resource, 810 Action: "push", 811 }) 812 case "DELETE": 813 // DELETE access requires full admin rights, which is represented 814 // as "*". This may not be ideal. 815 records = append(records, 816 auth.Access{ 817 Resource: resource, 818 Action: "*", 819 }) 820 } 821 return records 822 } 823 824 // Add the access record for the catalog if it's our current route 825 func appendCatalogAccessRecord(accessRecords []auth.Access, r *http.Request) []auth.Access { 826 route := mux.CurrentRoute(r) 827 routeName := route.GetName() 828 829 if routeName == v2.RouteNameCatalog { 830 resource := auth.Resource{ 831 Type: "registry", 832 Name: "catalog", 833 } 834 835 accessRecords = append(accessRecords, 836 auth.Access{ 837 Resource: resource, 838 Action: "*", 839 }) 840 } 841 return accessRecords 842 } 843 844 // applyRegistryMiddleware wraps a registry instance with the configured middlewares 845 func applyRegistryMiddleware(ctx context.Context, registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) { 846 for _, mw := range middlewares { 847 rmw, err := registrymiddleware.Get(ctx, mw.Name, mw.Options, registry) 848 if err != nil { 849 return nil, fmt.Errorf("unable to configure registry middleware (%s): %s", mw.Name, err) 850 } 851 registry = rmw 852 } 853 return registry, nil 854 855 } 856 857 // applyRepoMiddleware wraps a repository with the configured middlewares 858 func applyRepoMiddleware(ctx context.Context, repository distribution.Repository, middlewares []configuration.Middleware) (distribution.Repository, error) { 859 for _, mw := range middlewares { 860 rmw, err := repositorymiddleware.Get(ctx, mw.Name, mw.Options, repository) 861 if err != nil { 862 return nil, err 863 } 864 repository = rmw 865 } 866 return repository, nil 867 } 868 869 // applyStorageMiddleware wraps a storage driver with the configured middlewares 870 func applyStorageMiddleware(driver storagedriver.StorageDriver, middlewares []configuration.Middleware) (storagedriver.StorageDriver, error) { 871 for _, mw := range middlewares { 872 smw, err := storagemiddleware.Get(mw.Name, mw.Options, driver) 873 if err != nil { 874 return nil, fmt.Errorf("unable to configure storage middleware (%s): %v", mw.Name, err) 875 } 876 driver = smw 877 } 878 return driver, nil 879 } 880 881 // uploadPurgeDefaultConfig provides a default configuration for upload 882 // purging to be used in the absence of configuration in the 883 // confifuration file 884 func uploadPurgeDefaultConfig() map[interface{}]interface{} { 885 config := map[interface{}]interface{}{} 886 config["enabled"] = true 887 config["age"] = "168h" 888 config["interval"] = "24h" 889 config["dryrun"] = false 890 return config 891 } 892 893 func badPurgeUploadConfig(reason string) { 894 panic(fmt.Sprintf("Unable to parse upload purge configuration: %s", reason)) 895 } 896 897 // startUploadPurger schedules a goroutine which will periodically 898 // check upload directories for old files and delete them 899 func startUploadPurger(ctx context.Context, storageDriver storagedriver.StorageDriver, log ctxu.Logger, config map[interface{}]interface{}) { 900 if config["enabled"] == false { 901 return 902 } 903 904 var purgeAgeDuration time.Duration 905 var err error 906 purgeAge, ok := config["age"] 907 if ok { 908 ageStr, ok := purgeAge.(string) 909 if !ok { 910 badPurgeUploadConfig("age is not a string") 911 } 912 purgeAgeDuration, err = time.ParseDuration(ageStr) 913 if err != nil { 914 badPurgeUploadConfig(fmt.Sprintf("Cannot parse duration: %s", err.Error())) 915 } 916 } else { 917 badPurgeUploadConfig("age missing") 918 } 919 920 var intervalDuration time.Duration 921 interval, ok := config["interval"] 922 if ok { 923 intervalStr, ok := interval.(string) 924 if !ok { 925 badPurgeUploadConfig("interval is not a string") 926 } 927 928 intervalDuration, err = time.ParseDuration(intervalStr) 929 if err != nil { 930 badPurgeUploadConfig(fmt.Sprintf("Cannot parse interval: %s", err.Error())) 931 } 932 } else { 933 badPurgeUploadConfig("interval missing") 934 } 935 936 var dryRunBool bool 937 dryRun, ok := config["dryrun"] 938 if ok { 939 dryRunBool, ok = dryRun.(bool) 940 if !ok { 941 badPurgeUploadConfig("cannot parse dryrun") 942 } 943 } else { 944 badPurgeUploadConfig("dryrun missing") 945 } 946 947 go func() { 948 rand.Seed(time.Now().Unix()) 949 jitter := time.Duration(rand.Int()%60) * time.Minute 950 log.Infof("Starting upload purge in %s", jitter) 951 time.Sleep(jitter) 952 953 for { 954 storage.PurgeUploads(ctx, storageDriver, time.Now().Add(-purgeAgeDuration), !dryRunBool) 955 log.Infof("Starting upload purge in %s", intervalDuration) 956 time.Sleep(intervalDuration) 957 } 958 }() 959 }