2015-08-20 22:56:36 +02:00
package registry
import (
2017-08-12 00:31:16 +02:00
"context"
2015-08-20 22:56:36 +02:00
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"os"
2018-07-31 01:15:35 +02:00
"os/signal"
2021-02-18 22:31:23 +01:00
"strings"
2018-07-31 01:15:35 +02:00
"syscall"
2015-08-20 22:56:36 +02:00
"time"
2019-10-09 14:02:21 +02:00
logrus_bugsnag "github.com/Shopify/logrus-bugsnag"
2017-01-05 20:40:18 +01:00
logstash "github.com/bshuster-repo/logrus-logstash-hook"
2015-08-20 22:56:36 +02:00
"github.com/bugsnag/bugsnag-go"
2019-05-16 02:21:50 +02:00
"github.com/docker/go-metrics"
gorhandlers "github.com/gorilla/handlers"
2020-09-01 02:26:57 +02:00
"github.com/sirupsen/logrus"
2019-05-16 02:21:50 +02:00
"github.com/spf13/cobra"
"github.com/yvasiyarov/gorelic"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
2020-08-24 13:18:39 +02:00
"github.com/distribution/distribution/v3/configuration"
dcontext "github.com/distribution/distribution/v3/context"
"github.com/distribution/distribution/v3/health"
"github.com/distribution/distribution/v3/registry/handlers"
"github.com/distribution/distribution/v3/registry/listener"
"github.com/distribution/distribution/v3/uuid"
"github.com/distribution/distribution/v3/version"
2015-08-20 22:56:36 +02:00
)
2021-02-18 22:31:23 +01:00
// a map of TLS cipher suite names to constants in https://golang.org/pkg/crypto/tls/#pkg-constants
var cipherSuites = map [ string ] uint16 {
// TLS 1.0 - 1.2 cipher suites
"TLS_RSA_WITH_3DES_EDE_CBC_SHA" : tls . TLS_RSA_WITH_3DES_EDE_CBC_SHA ,
"TLS_RSA_WITH_AES_128_CBC_SHA" : tls . TLS_RSA_WITH_AES_128_CBC_SHA ,
"TLS_RSA_WITH_AES_256_CBC_SHA" : tls . TLS_RSA_WITH_AES_256_CBC_SHA ,
"TLS_RSA_WITH_AES_128_GCM_SHA256" : tls . TLS_RSA_WITH_AES_128_GCM_SHA256 ,
"TLS_RSA_WITH_AES_256_GCM_SHA384" : tls . TLS_RSA_WITH_AES_256_GCM_SHA384 ,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" : tls . TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA ,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" : tls . TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA ,
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA" : tls . TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA ,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" : tls . TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA ,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" : tls . TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA ,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" : tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" : tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" : tls . TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" : tls . TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" : tls . TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" : tls . TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ,
// TLS 1.3 cipher suites
"TLS_AES_128_GCM_SHA256" : tls . TLS_AES_128_GCM_SHA256 ,
"TLS_AES_256_GCM_SHA384" : tls . TLS_AES_256_GCM_SHA384 ,
"TLS_CHACHA20_POLY1305_SHA256" : tls . TLS_CHACHA20_POLY1305_SHA256 ,
}
// a list of default ciphersuites to utilize
var defaultCipherSuites = [ ] uint16 {
tls . TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ,
tls . TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ,
tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_AES_128_GCM_SHA256 ,
tls . TLS_CHACHA20_POLY1305_SHA256 ,
tls . TLS_AES_256_GCM_SHA384 ,
}
// maps tls version strings to constants
var defaultTLSVersionStr = "tls1.2"
var tlsVersions = map [ string ] uint16 {
// user specified values
"tls1.2" : tls . VersionTLS12 ,
"tls1.3" : tls . VersionTLS13 ,
}
2018-07-31 01:15:35 +02:00
// this channel gets notified when process receives signal. It is global to ease unit testing
var quit = make ( chan os . Signal , 1 )
2016-01-19 23:26:15 +01:00
// ServeCmd is a cobra command for running the registry.
var ServeCmd = & cobra . Command {
Use : "serve <config>" ,
Short : "`serve` stores and distributes Docker images" ,
Long : "`serve` stores and distributes Docker images." ,
2015-08-21 00:43:08 +02:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2015-09-11 05:41:58 +02:00
// setup context
2017-08-12 00:31:16 +02:00
ctx := dcontext . WithVersion ( dcontext . Background ( ) , version . Version )
2015-09-11 05:41:58 +02:00
2015-08-21 00:43:08 +02:00
config , err := resolveConfiguration ( args )
if err != nil {
fmt . Fprintf ( os . Stderr , "configuration error: %v\n" , err )
cmd . Usage ( )
os . Exit ( 1 )
}
if config . HTTP . Debug . Addr != "" {
go func ( addr string ) {
2021-11-15 07:57:22 +01:00
logrus . Infof ( "debug server listening %v" , addr )
2015-08-21 00:43:08 +02:00
if err := http . ListenAndServe ( addr , nil ) ; err != nil {
2021-11-15 07:57:22 +01:00
logrus . Fatalf ( "error listening on debug interface: %v" , err )
2015-08-21 00:43:08 +02:00
}
} ( config . HTTP . Debug . Addr )
}
2015-09-11 05:41:58 +02:00
registry , err := NewRegistry ( ctx , config )
2015-08-21 00:43:08 +02:00
if err != nil {
2021-11-15 07:57:22 +01:00
logrus . Fatalln ( err )
2015-08-21 00:43:08 +02:00
}
2017-11-17 01:43:38 +01:00
if config . HTTP . Debug . Prometheus . Enabled {
path := config . HTTP . Debug . Prometheus . Path
if path == "" {
path = "/metrics"
}
2021-11-15 07:57:22 +01:00
logrus . Info ( "providing prometheus metrics on " , path )
2017-11-17 01:43:38 +01:00
http . Handle ( path , metrics . Handler ( ) )
}
2015-08-21 00:43:08 +02:00
if err = registry . ListenAndServe ( ) ; err != nil {
2021-11-15 07:57:22 +01:00
logrus . Fatalln ( err )
2015-08-21 00:43:08 +02:00
}
} ,
}
2015-08-20 22:56:36 +02:00
// A Registry represents a complete instance of the registry.
2015-08-21 00:43:08 +02:00
// TODO(aaronl): It might make sense for Registry to become an interface.
2015-08-20 22:56:36 +02:00
type Registry struct {
2015-08-21 00:43:08 +02:00
config * configuration . Configuration
app * handlers . App
server * http . Server
2015-08-20 22:56:36 +02:00
}
// NewRegistry creates a new registry from a context and configuration struct.
func NewRegistry ( ctx context . Context , config * configuration . Configuration ) ( * Registry , error ) {
var err error
ctx , err = configureLogging ( ctx , config )
if err != nil {
return nil , fmt . Errorf ( "error configuring logger: %v" , err )
}
2018-10-25 23:38:26 +02:00
configureBugsnag ( config )
2015-08-20 22:56:36 +02:00
// inject a logger into the uuid library. warns us if there is a problem
// with uuid generation under low entropy.
2017-08-12 00:31:16 +02:00
uuid . Loggerf = dcontext . GetLogger ( ctx ) . Warnf
2015-08-20 22:56:36 +02:00
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 )
2016-09-14 02:23:27 +02:00
if ! config . Log . AccessLog . Disabled {
handler = gorhandlers . CombinedLoggingHandler ( os . Stdout , handler )
}
2015-08-20 22:56:36 +02:00
server := & http . Server {
Handler : handler ,
}
2015-08-21 00:43:08 +02:00
return & Registry {
app : app ,
config : config ,
server : server ,
} , nil
}
2021-02-18 22:31:23 +01:00
// takes a list of cipher suites and converts it to a list of respective tls constants
// if an empty list is provided, then the defaults will be used
func getCipherSuites ( names [ ] string ) ( [ ] uint16 , error ) {
if len ( names ) == 0 {
return defaultCipherSuites , nil
}
cipherSuiteConsts := make ( [ ] uint16 , len ( names ) )
for i , name := range names {
cipherSuiteConst , ok := cipherSuites [ name ]
if ! ok {
return nil , fmt . Errorf ( "unknown TLS cipher suite '%s' specified for http.tls.cipherSuites" , name )
}
cipherSuiteConsts [ i ] = cipherSuiteConst
}
return cipherSuiteConsts , nil
}
// takes a list of cipher suite ids and converts it to a list of respective names
func getCipherSuiteNames ( ids [ ] uint16 ) [ ] string {
if len ( ids ) == 0 {
return nil
}
names := make ( [ ] string , len ( ids ) )
for i , id := range ids {
names [ i ] = tls . CipherSuiteName ( id )
}
return names
}
2015-08-21 00:43:08 +02:00
// ListenAndServe runs the registry's HTTP server.
func ( registry * Registry ) ListenAndServe ( ) error {
config := registry . config
2015-08-20 22:56:36 +02:00
ln , err := listener . NewListener ( config . HTTP . Net , config . HTTP . Addr )
if err != nil {
2015-08-21 00:43:08 +02:00
return err
2015-08-20 22:56:36 +02:00
}
2016-06-13 20:30:42 +02:00
if config . HTTP . TLS . Certificate != "" || config . HTTP . TLS . LetsEncrypt . CacheFile != "" {
2019-01-09 03:29:40 +01:00
if config . HTTP . TLS . MinimumTLS == "" {
2021-02-18 22:31:23 +01:00
config . HTTP . TLS . MinimumTLS = defaultTLSVersionStr
2019-01-09 03:29:40 +01:00
}
2021-02-18 22:31:23 +01:00
tlsMinVersion , ok := tlsVersions [ config . HTTP . TLS . MinimumTLS ]
if ! ok {
return fmt . Errorf ( "unknown minimum TLS level '%s' specified for http.tls.minimumtls" , config . HTTP . TLS . MinimumTLS )
}
dcontext . GetLogger ( registry . app ) . Infof ( "restricting TLS version to %s or higher" , config . HTTP . TLS . MinimumTLS )
2022-01-24 13:02:57 +01:00
var tlsCipherSuites [ ] uint16
// configuring cipher suites are no longer supported after the tls1.3.
// (https://go.dev/blog/tls-cipher-suites)
if tlsMinVersion > tls . VersionTLS12 {
dcontext . GetLogger ( registry . app ) . Warnf ( "restricting TLS cipher suites to empty. Because configuring cipher suites is no longer supported in %s" , config . HTTP . TLS . MinimumTLS )
} else {
tlsCipherSuites , err = getCipherSuites ( config . HTTP . TLS . CipherSuites )
if err != nil {
return err
}
dcontext . GetLogger ( registry . app ) . Infof ( "restricting TLS cipher suites to: %s" , strings . Join ( getCipherSuiteNames ( tlsCipherSuites ) , "," ) )
2021-02-18 22:31:23 +01:00
}
2015-08-20 22:56:36 +02:00
tlsConf := & tls . Config {
ClientAuth : tls . NoClientCert ,
2016-07-14 21:48:03 +02:00
NextProtos : nextProtos ( config ) ,
2019-01-09 03:29:40 +01:00
MinVersion : tlsMinVersion ,
2015-08-20 22:56:36 +02:00
PreferServerCipherSuites : true ,
2021-02-18 22:31:23 +01:00
CipherSuites : tlsCipherSuites ,
2015-08-20 22:56:36 +02:00
}
2016-06-13 20:30:42 +02:00
if config . HTTP . TLS . LetsEncrypt . CacheFile != "" {
if config . HTTP . TLS . Certificate != "" {
return fmt . Errorf ( "cannot specify both certificate and Let's Encrypt" )
}
2019-05-16 02:21:50 +02:00
m := & autocert . Manager {
HostPolicy : autocert . HostWhitelist ( config . HTTP . TLS . LetsEncrypt . Hosts ... ) ,
Cache : autocert . DirCache ( config . HTTP . TLS . LetsEncrypt . CacheFile ) ,
Email : config . HTTP . TLS . LetsEncrypt . Email ,
Prompt : autocert . AcceptTOS ,
2018-02-01 21:16:58 +01:00
}
2016-06-13 20:30:42 +02:00
tlsConf . GetCertificate = m . GetCertificate
2019-05-16 02:21:50 +02:00
tlsConf . NextProtos = append ( tlsConf . NextProtos , acme . ALPNProto )
2016-06-13 20:30:42 +02:00
} else {
tlsConf . Certificates = make ( [ ] tls . Certificate , 1 )
tlsConf . Certificates [ 0 ] , err = tls . LoadX509KeyPair ( config . HTTP . TLS . Certificate , config . HTTP . TLS . Key )
if err != nil {
return err
}
2015-08-20 22:56:36 +02:00
}
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 {
2015-08-21 00:43:08 +02:00
return err
2015-08-20 22:56:36 +02:00
}
if ok := pool . AppendCertsFromPEM ( caPem ) ; ! ok {
2019-02-05 01:01:04 +01:00
return fmt . Errorf ( "could not add CA to pool" )
2015-08-20 22:56:36 +02:00
}
}
for _ , subj := range pool . Subjects ( ) {
2017-08-12 00:31:16 +02:00
dcontext . GetLogger ( registry . app ) . Debugf ( "CA Subject: %s" , string ( subj ) )
2015-08-20 22:56:36 +02:00
}
tlsConf . ClientAuth = tls . RequireAndVerifyClientCert
tlsConf . ClientCAs = pool
}
ln = tls . NewListener ( ln , tlsConf )
2017-08-12 00:31:16 +02:00
dcontext . GetLogger ( registry . app ) . Infof ( "listening on %v, tls" , ln . Addr ( ) )
2015-08-20 22:56:36 +02:00
} else {
2017-08-12 00:31:16 +02:00
dcontext . GetLogger ( registry . app ) . Infof ( "listening on %v" , ln . Addr ( ) )
2015-08-20 22:56:36 +02:00
}
2018-07-31 01:15:35 +02:00
if config . HTTP . DrainTimeout == 0 {
return registry . server . Serve ( ln )
}
// setup channel to get notified on SIGTERM signal
signal . Notify ( quit , syscall . SIGTERM )
serveErr := make ( chan error )
// Start serving in goroutine and listen for stop signal in main thread
go func ( ) {
serveErr <- registry . server . Serve ( ln )
} ( )
select {
case err := <- serveErr :
return err
case <- quit :
dcontext . GetLogger ( registry . app ) . Info ( "stopping server gracefully. Draining connections for " , config . HTTP . DrainTimeout )
// shutdown the server with a grace period of configured timeout
c , cancel := context . WithTimeout ( context . Background ( ) , config . HTTP . DrainTimeout )
defer cancel ( )
return registry . server . Shutdown ( c )
}
2015-08-20 22:56:36 +02:00
}
func configureReporting ( app * handlers . App ) http . Handler {
var handler http . Handler = app
if app . Config . Reporting . Bugsnag . APIKey != "" {
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.
2015-09-11 18:54:15 +02:00
func configureLogging ( ctx context . Context , config * configuration . Configuration ) ( context . Context , error ) {
2021-11-15 07:57:22 +01:00
logrus . SetLevel ( logLevel ( config . Log . Level ) )
2015-08-20 22:56:36 +02:00
formatter := config . Log . Formatter
if formatter == "" {
formatter = "text" // default formatter
}
switch formatter {
case "json" :
2021-11-15 07:57:22 +01:00
logrus . SetFormatter ( & logrus . JSONFormatter {
2021-02-15 19:16:27 +01:00
TimestampFormat : time . RFC3339Nano ,
DisableHTMLEscape : true ,
2015-08-20 22:56:36 +02:00
} )
case "text" :
2021-11-15 07:57:22 +01:00
logrus . SetFormatter ( & logrus . TextFormatter {
2015-08-20 22:56:36 +02:00
TimestampFormat : time . RFC3339Nano ,
} )
case "logstash" :
2021-11-15 07:57:22 +01:00
logrus . SetFormatter ( & logstash . LogstashFormatter {
2020-09-01 02:26:57 +02:00
Formatter : & logrus . JSONFormatter { TimestampFormat : time . RFC3339Nano } ,
2015-08-20 22:56:36 +02:00
} )
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 != "" {
2021-11-15 07:57:22 +01:00
logrus . Debugf ( "using %q logging formatter" , config . Log . Formatter )
2015-08-20 22:56:36 +02:00
}
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 )
}
2017-08-12 00:31:16 +02:00
ctx = dcontext . WithValues ( ctx , config . Log . Fields )
ctx = dcontext . WithLogger ( ctx , dcontext . GetLogger ( ctx , fields ... ) )
2015-08-20 22:56:36 +02:00
}
2020-04-01 00:05:16 +02:00
dcontext . SetDefaultLogger ( dcontext . GetLogger ( ctx ) )
2015-08-20 22:56:36 +02:00
return ctx , nil
}
2021-11-15 07:57:22 +01:00
func logLevel ( level configuration . Loglevel ) logrus . Level {
l , err := logrus . ParseLevel ( string ( level ) )
2015-08-20 22:56:36 +02:00
if err != nil {
2021-11-15 07:57:22 +01:00
l = logrus . InfoLevel
logrus . Warnf ( "error parsing level %q: %v, using %q " , level , err , l )
2015-08-20 22:56:36 +02:00
}
return l
}
2018-10-25 23:38:26 +02:00
// configureBugsnag configures bugsnag reporting, if enabled
func configureBugsnag ( config * configuration . Configuration ) {
if config . Reporting . Bugsnag . APIKey == "" {
return
}
bugsnagConfig := bugsnag . Configuration {
APIKey : config . Reporting . Bugsnag . APIKey ,
}
if config . Reporting . Bugsnag . ReleaseStage != "" {
bugsnagConfig . ReleaseStage = config . Reporting . Bugsnag . ReleaseStage
}
if config . Reporting . Bugsnag . Endpoint != "" {
bugsnagConfig . Endpoint = config . Reporting . Bugsnag . Endpoint
}
bugsnag . Configure ( bugsnagConfig )
// configure logrus bugsnag hook
hook , err := logrus_bugsnag . NewBugsnagHook ( )
if err != nil {
2021-11-15 07:57:22 +01:00
logrus . Fatalln ( err )
2018-10-25 23:38:26 +02:00
}
2021-11-15 07:57:22 +01:00
logrus . AddHook ( hook )
2018-10-25 23:38:26 +02:00
}
2016-06-02 07:31:13 +02:00
// panicHandler add an HTTP handler to web app. The handler recover the happening
2015-08-20 22:56:36 +02:00
// 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 {
2021-11-15 07:57:22 +01:00
logrus . Panic ( fmt . Sprintf ( "%v" , err ) )
2015-08-20 22:56:36 +02:00
}
} ( )
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 )
} )
}
2015-08-21 00:43:08 +02:00
func resolveConfiguration ( args [ ] string ) ( * configuration . Configuration , error ) {
var configurationPath string
if len ( args ) > 0 {
configurationPath = args [ 0 ]
} else if os . Getenv ( "REGISTRY_CONFIGURATION_PATH" ) != "" {
configurationPath = os . Getenv ( "REGISTRY_CONFIGURATION_PATH" )
}
if configurationPath == "" {
return nil , fmt . Errorf ( "configuration path unspecified" )
}
fp , err := os . Open ( configurationPath )
if err != nil {
return nil , err
}
defer fp . Close ( )
config , err := configuration . Parse ( fp )
if err != nil {
return nil , fmt . Errorf ( "error parsing %s: %v" , configurationPath , err )
}
return config , nil
}
2016-07-14 21:48:03 +02:00
func nextProtos ( config * configuration . Configuration ) [ ] string {
switch config . HTTP . HTTP2 . Disabled {
case true :
return [ ] string { "http/1.1" }
default :
return [ ] string { "h2" , "http/1.1" }
}
}