2015-08-20 20:56:36 +00:00
package registry
import (
2017-08-11 22:31:16 +00:00
"context"
2015-08-20 20:56:36 +00:00
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
2018-07-30 23:15:35 +00:00
"os/signal"
2021-02-18 21:31:23 +00:00
"strings"
2018-07-30 23:15:35 +00:00
"syscall"
2015-08-20 20:56:36 +00:00
"time"
2017-01-05 19:40:18 +00:00
logstash "github.com/bshuster-repo/logrus-logstash-hook"
2019-05-16 00:21:50 +00:00
"github.com/docker/go-metrics"
gorhandlers "github.com/gorilla/handlers"
2020-09-01 00:26:57 +00:00
"github.com/sirupsen/logrus"
2019-05-16 00:21:50 +00:00
"github.com/spf13/cobra"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
2020-08-24 11:18:39 +00: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/version"
2015-08-20 20:56:36 +00:00
)
2021-02-18 21:31:23 +00: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 ,
}
2022-11-05 11:40:28 +00:00
const defaultTLSVersionStr = "tls1.2"
// tlsVersions maps user-specified values to tls version constants.
var tlsVersions = map [ string ] uint16 {
"tls1.2" : tls . VersionTLS12 ,
"tls1.3" : tls . VersionTLS13 ,
}
// defaultLogFormatter is the default formatter to use for logs.
const defaultLogFormatter = "text"
2021-02-18 21:31:23 +00:00
2018-07-30 23:15:35 +00:00
// this channel gets notified when process receives signal. It is global to ease unit testing
var quit = make ( chan os . Signal , 1 )
2016-11-01 23:44:18 +00:00
// HandlerFunc defines an http middleware
type HandlerFunc func ( config * configuration . Configuration , handler http . Handler ) http . Handler
var handlerMiddlewares [ ] HandlerFunc
// RegisterHandler is used to register http middlewares to the registry service
func RegisterHandler ( handlerFunc HandlerFunc ) {
handlerMiddlewares = append ( handlerMiddlewares , handlerFunc )
}
2016-01-19 22:26:15 +00: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-20 22:43:08 +00:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2015-09-11 03:41:58 +00:00
// setup context
2017-08-11 22:31:16 +00:00
ctx := dcontext . WithVersion ( dcontext . Background ( ) , version . Version )
2015-09-11 03:41:58 +00:00
2015-08-20 22:43:08 +00:00
config , err := resolveConfiguration ( args )
if err != nil {
fmt . Fprintf ( os . Stderr , "configuration error: %v\n" , err )
cmd . Usage ( )
os . Exit ( 1 )
}
2015-09-11 03:41:58 +00:00
registry , err := NewRegistry ( ctx , config )
2015-08-20 22:43:08 +00:00
if err != nil {
2021-11-15 06:57:22 +00:00
logrus . Fatalln ( err )
2015-08-20 22:43:08 +00:00
}
2023-05-03 16:35:05 +00:00
configureDebugServer ( config )
2017-11-17 00:43:38 +00:00
2015-08-20 22:43:08 +00:00
if err = registry . ListenAndServe ( ) ; err != nil {
2021-11-15 06:57:22 +00:00
logrus . Fatalln ( err )
2015-08-20 22:43:08 +00:00
}
} ,
}
2015-08-20 20:56:36 +00:00
// A Registry represents a complete instance of the registry.
2022-11-05 11:40:28 +00:00
//
2015-08-20 22:43:08 +00:00
// TODO(aaronl): It might make sense for Registry to become an interface.
2015-08-20 20:56:36 +00:00
type Registry struct {
2015-08-20 22:43:08 +00:00
config * configuration . Configuration
app * handlers . App
server * http . Server
2015-08-20 20:56:36 +00: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 )
}
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 ( )
2023-08-21 08:24:36 +00:00
var handler http . Handler = app
2015-08-20 20:56:36 +00:00
handler = alive ( "/" , handler )
handler = health . Handler ( handler )
handler = panicHandler ( handler )
2016-09-14 00:23:27 +00:00
if ! config . Log . AccessLog . Disabled {
handler = gorhandlers . CombinedLoggingHandler ( os . Stdout , handler )
}
2015-08-20 20:56:36 +00:00
2016-11-01 23:44:18 +00:00
for _ , applyHandlerMiddleware := range handlerMiddlewares {
handler = applyHandlerMiddleware ( config , handler )
2016-09-14 00:23:27 +00:00
}
2015-08-20 20:56:36 +00:00
server := & http . Server {
Handler : handler ,
}
2015-08-20 22:43:08 +00:00
return & Registry {
app : app ,
config : config ,
server : server ,
} , nil
}
2021-02-18 21:31:23 +00: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
}
2021-07-23 21:25:09 +00:00
// set ACME-server/DirectoryURL, if provided
func setDirectoryURL ( directoryurl string ) * acme . Client {
if len ( directoryurl ) > 0 {
return & acme . Client { DirectoryURL : directoryurl }
}
return nil
}
2015-08-20 22:43:08 +00:00
// ListenAndServe runs the registry's HTTP server.
func ( registry * Registry ) ListenAndServe ( ) error {
config := registry . config
2015-08-20 20:56:36 +00:00
ln , err := listener . NewListener ( config . HTTP . Net , config . HTTP . Addr )
if err != nil {
2015-08-20 22:43:08 +00:00
return err
2015-08-20 20:56:36 +00:00
}
2016-06-13 18:30:42 +00:00
if config . HTTP . TLS . Certificate != "" || config . HTTP . TLS . LetsEncrypt . CacheFile != "" {
2019-01-09 02:29:40 +00:00
if config . HTTP . TLS . MinimumTLS == "" {
2021-02-18 21:31:23 +00:00
config . HTTP . TLS . MinimumTLS = defaultTLSVersionStr
2019-01-09 02:29:40 +00:00
}
2021-02-18 21:31:23 +00: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 12:02:57 +00: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 21:31:23 +00:00
}
2015-08-20 20:56:36 +00:00
tlsConf := & tls . Config {
2023-05-09 11:19:48 +00:00
ClientAuth : tls . NoClientCert ,
NextProtos : nextProtos ( config ) ,
MinVersion : tlsMinVersion ,
CipherSuites : tlsCipherSuites ,
2015-08-20 20:56:36 +00:00
}
2016-06-13 18:30:42 +00: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 00:21:50 +00: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 ,
2021-07-23 21:25:09 +00:00
Client : setDirectoryURL ( config . HTTP . TLS . LetsEncrypt . DirectoryURL ) ,
2018-02-01 20:16:58 +00:00
}
2016-06-13 18:30:42 +00:00
tlsConf . GetCertificate = m . GetCertificate
2019-05-16 00:21:50 +00:00
tlsConf . NextProtos = append ( tlsConf . NextProtos , acme . ALPNProto )
2016-06-13 18:30:42 +00: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 20:56:36 +00:00
}
if len ( config . HTTP . TLS . ClientCAs ) != 0 {
pool := x509 . NewCertPool ( )
for _ , ca := range config . HTTP . TLS . ClientCAs {
2022-11-02 21:55:22 +00:00
caPem , err := os . ReadFile ( ca )
2015-08-20 20:56:36 +00:00
if err != nil {
2015-08-20 22:43:08 +00:00
return err
2015-08-20 20:56:36 +00:00
}
if ok := pool . AppendCertsFromPEM ( caPem ) ; ! ok {
2019-02-05 00:01:04 +00:00
return fmt . Errorf ( "could not add CA to pool" )
2015-08-20 20:56:36 +00:00
}
}
2023-05-09 11:19:48 +00:00
for _ , subj := range pool . Subjects ( ) { //nolint:staticcheck // FIXME(thaJeztah): ignore SA1019: ac.(*accessController).rootCerts.Subjects has been deprecated since Go 1.18: if s was returned by SystemCertPool, Subjects will not include the system roots. (staticcheck)
2017-08-11 22:31:16 +00:00
dcontext . GetLogger ( registry . app ) . Debugf ( "CA Subject: %s" , string ( subj ) )
2015-08-20 20:56:36 +00:00
}
tlsConf . ClientAuth = tls . RequireAndVerifyClientCert
tlsConf . ClientCAs = pool
}
ln = tls . NewListener ( ln , tlsConf )
2017-08-11 22:31:16 +00:00
dcontext . GetLogger ( registry . app ) . Infof ( "listening on %v, tls" , ln . Addr ( ) )
2015-08-20 20:56:36 +00:00
} else {
2017-08-11 22:31:16 +00:00
dcontext . GetLogger ( registry . app ) . Infof ( "listening on %v" , ln . Addr ( ) )
2015-08-20 20:56:36 +00:00
}
2018-07-30 23:15:35 +00: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 20:56:36 +00:00
}
2023-05-03 16:35:05 +00:00
func configureDebugServer ( config * configuration . Configuration ) {
if config . HTTP . Debug . Addr != "" {
go func ( addr string ) {
2023-05-03 16:56:17 +00:00
logrus . Infof ( "debug server listening %v" , addr )
2023-05-03 16:35:05 +00:00
if err := http . ListenAndServe ( addr , nil ) ; err != nil {
2023-05-03 16:56:17 +00:00
logrus . Fatalf ( "error listening on debug interface: %v" , err )
2023-05-03 16:35:05 +00:00
}
} ( config . HTTP . Debug . Addr )
configurePrometheus ( config )
}
}
func configurePrometheus ( config * configuration . Configuration ) {
if config . HTTP . Debug . Prometheus . Enabled {
path := config . HTTP . Debug . Prometheus . Path
if path == "" {
path = "/metrics"
}
2023-05-03 16:56:17 +00:00
logrus . Info ( "providing prometheus metrics on " , path )
2023-05-03 16:35:05 +00:00
http . Handle ( path , metrics . Handler ( ) )
}
}
2015-08-20 20:56:36 +00:00
// configureLogging prepares the context with a logger using the
// configuration.
2015-09-11 16:54:15 +00:00
func configureLogging ( ctx context . Context , config * configuration . Configuration ) ( context . Context , error ) {
2021-11-15 06:57:22 +00:00
logrus . SetLevel ( logLevel ( config . Log . Level ) )
2023-06-02 07:46:53 +00:00
logrus . SetReportCaller ( config . Log . ReportCaller )
2015-08-20 20:56:36 +00:00
formatter := config . Log . Formatter
if formatter == "" {
2022-11-05 11:40:28 +00:00
formatter = defaultLogFormatter
2015-08-20 20:56:36 +00:00
}
switch formatter {
case "json" :
2021-11-15 06:57:22 +00:00
logrus . SetFormatter ( & logrus . JSONFormatter {
2021-02-15 18:16:27 +00:00
TimestampFormat : time . RFC3339Nano ,
DisableHTMLEscape : true ,
2015-08-20 20:56:36 +00:00
} )
case "text" :
2021-11-15 06:57:22 +00:00
logrus . SetFormatter ( & logrus . TextFormatter {
2015-08-20 20:56:36 +00:00
TimestampFormat : time . RFC3339Nano ,
} )
case "logstash" :
2021-11-15 06:57:22 +00:00
logrus . SetFormatter ( & logstash . LogstashFormatter {
2020-09-01 00:26:57 +00:00
Formatter : & logrus . JSONFormatter { TimestampFormat : time . RFC3339Nano } ,
2015-08-20 20:56:36 +00:00
} )
default :
2022-11-04 23:05:39 +00:00
return ctx , fmt . Errorf ( "unsupported logging formatter: %q" , formatter )
2015-08-20 20:56:36 +00:00
}
2022-11-04 23:05:39 +00:00
logrus . Debugf ( "using %q logging formatter" , formatter )
2015-08-20 20:56:36 +00: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-11 22:31:16 +00:00
ctx = dcontext . WithValues ( ctx , config . Log . Fields )
ctx = dcontext . WithLogger ( ctx , dcontext . GetLogger ( ctx , fields ... ) )
2015-08-20 20:56:36 +00:00
}
2020-03-31 22:05:16 +00:00
dcontext . SetDefaultLogger ( dcontext . GetLogger ( ctx ) )
2015-08-20 20:56:36 +00:00
return ctx , nil
}
2021-11-15 06:57:22 +00:00
func logLevel ( level configuration . Loglevel ) logrus . Level {
l , err := logrus . ParseLevel ( string ( level ) )
2015-08-20 20:56:36 +00:00
if err != nil {
2021-11-15 06:57:22 +00:00
l = logrus . InfoLevel
logrus . Warnf ( "error parsing level %q: %v, using %q " , level , err , l )
2015-08-20 20:56:36 +00:00
}
return l
}
2016-06-02 05:31:13 +00:00
// panicHandler add an HTTP handler to web app. The handler recover the happening
2015-08-20 20:56:36 +00: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 06:57:22 +00:00
logrus . Panic ( fmt . Sprintf ( "%v" , err ) )
2015-08-20 20:56:36 +00: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-20 22:43:08 +00: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 19:48:03 +00: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" }
}
}