package registry import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net" "net/http" "os" "time" log "github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus/formatters/logstash" "github.com/bugsnag/bugsnag-go" "github.com/docker/distribution/configuration" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/health" "github.com/docker/distribution/registry/handlers" "github.com/docker/distribution/registry/listener" "github.com/docker/distribution/uuid" "github.com/docker/distribution/version" gorhandlers "github.com/gorilla/handlers" "github.com/yvasiyarov/gorelic" "golang.org/x/net/context" ) // A Registry represents a complete instance of the registry. type Registry struct { config *configuration.Configuration app *handlers.App server *http.Server ln net.Listener debugLn net.Listener } // NewRegistry creates a new registry from a context and configuration struct. func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Registry, error) { // Note this ctx = ctxu.WithValue(ctx, "version", version.Version) var err error ctx, err = configureLogging(ctx, config) if err != nil { return nil, fmt.Errorf("error configuring logger: %v", err) } // inject a logger into the uuid library. warns us if there is a problem // with uuid generation under low entropy. uuid.Loggerf = ctxu.GetLogger(ctx).Warnf app := handlers.NewApp(ctx, config) // TODO(aaronl): The global scope of the health checks means NewRegistry // can only be called once per process. app.RegisterHealthChecks() handler := configureReporting(app) handler = alive("/", handler) handler = health.Handler(handler) handler = panicHandler(handler) handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) server := &http.Server{ Handler: handler, } ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr) if err != nil { return nil, err } var debugLn net.Listener if config.HTTP.Debug.Addr != "" { debugLn, err = listener.NewListener("tcp", config.HTTP.Debug.Addr) if err != nil { return nil, fmt.Errorf("error listening on debug interface: %v", err) } log.Infof("debug server listening %v", config.HTTP.Debug.Addr) } if config.HTTP.TLS.Certificate != "" { tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, NextProtos: []string{"http/1.1"}, Certificates: make([]tls.Certificate, 1), MinVersion: tls.VersionTLS10, PreferServerCipherSuites: true, CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, }, } tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key) if err != nil { return nil, err } if len(config.HTTP.TLS.ClientCAs) != 0 { pool := x509.NewCertPool() for _, ca := range config.HTTP.TLS.ClientCAs { caPem, err := ioutil.ReadFile(ca) if err != nil { return nil, err } if ok := pool.AppendCertsFromPEM(caPem); !ok { return nil, fmt.Errorf("Could not add CA to pool") } } for _, subj := range pool.Subjects() { ctxu.GetLogger(app).Debugf("CA Subject: %s", string(subj)) } tlsConf.ClientAuth = tls.RequireAndVerifyClientCert tlsConf.ClientCAs = pool } ln = tls.NewListener(ln, tlsConf) ctxu.GetLogger(app).Infof("listening on %v, tls", ln.Addr()) } else { ctxu.GetLogger(app).Infof("listening on %v", ln.Addr()) } return &Registry{ app: app, config: config, server: server, ln: ln, debugLn: debugLn, }, nil } // Serve runs the registry's HTTP server(s). func (registry *Registry) Serve() error { defer registry.ln.Close() errChan := make(chan error) if registry.debugLn != nil { defer registry.debugLn.Close() go func() { errChan <- http.Serve(registry.debugLn, nil) }() } go func() { errChan <- registry.server.Serve(registry.ln) }() return <-errChan } func configureReporting(app *handlers.App) http.Handler { var handler http.Handler = app if app.Config.Reporting.Bugsnag.APIKey != "" { bugsnagConfig := bugsnag.Configuration{ APIKey: app.Config.Reporting.Bugsnag.APIKey, // TODO(brianbland): provide the registry version here // AppVersion: "2.0", } if app.Config.Reporting.Bugsnag.ReleaseStage != "" { bugsnagConfig.ReleaseStage = app.Config.Reporting.Bugsnag.ReleaseStage } if app.Config.Reporting.Bugsnag.Endpoint != "" { bugsnagConfig.Endpoint = app.Config.Reporting.Bugsnag.Endpoint } bugsnag.Configure(bugsnagConfig) handler = bugsnag.Handler(handler) } if app.Config.Reporting.NewRelic.LicenseKey != "" { agent := gorelic.NewAgent() agent.NewrelicLicense = app.Config.Reporting.NewRelic.LicenseKey if app.Config.Reporting.NewRelic.Name != "" { agent.NewrelicName = app.Config.Reporting.NewRelic.Name } agent.CollectHTTPStat = true agent.Verbose = app.Config.Reporting.NewRelic.Verbose agent.Run() handler = agent.WrapHTTPHandler(handler) } return handler } // configureLogging prepares the context with a logger using the // configuration. func configureLogging(ctx ctxu.Context, config *configuration.Configuration) (context.Context, error) { if config.Log.Level == "" && config.Log.Formatter == "" { // If no config for logging is set, fallback to deprecated "Loglevel". log.SetLevel(logLevel(config.Loglevel)) ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version")) return ctx, nil } log.SetLevel(logLevel(config.Log.Level)) formatter := config.Log.Formatter if formatter == "" { formatter = "text" // default formatter } switch formatter { case "json": log.SetFormatter(&log.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) case "text": log.SetFormatter(&log.TextFormatter{ TimestampFormat: time.RFC3339Nano, }) case "logstash": log.SetFormatter(&logstash.LogstashFormatter{ TimestampFormat: time.RFC3339Nano, }) default: // just let the library use default on empty string. if config.Log.Formatter != "" { return ctx, fmt.Errorf("unsupported logging formatter: %q", config.Log.Formatter) } } if config.Log.Formatter != "" { log.Debugf("using %q logging formatter", config.Log.Formatter) } // log the application version with messages ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version")) if len(config.Log.Fields) > 0 { // build up the static fields, if present. var fields []interface{} for k := range config.Log.Fields { fields = append(fields, k) } ctx = ctxu.WithValues(ctx, config.Log.Fields) ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, fields...)) } return ctx, nil } func logLevel(level configuration.Loglevel) log.Level { l, err := log.ParseLevel(string(level)) if err != nil { l = log.InfoLevel log.Warnf("error parsing level %q: %v, using %q ", level, err, l) } return l } // panicHandler add a HTTP handler to web app. The handler recover the happening // panic. logrus.Panic transmits panic message to pre-config log hooks, which is // defined in config.yml. func panicHandler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Panic(fmt.Sprintf("%v", err)) } }() handler.ServeHTTP(w, r) }) } // alive simply wraps the handler with a route that always returns an http 200 // response when the path is matched. If the path is not matched, the request // is passed to the provided handler. There is no guarantee of anything but // that the server is up. Wrap with other handlers (such as health.Handler) // for greater affect. func alive(path string, handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == path { w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(http.StatusOK) return } handler.ServeHTTP(w, r) }) }