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