595 lines · 19.4 KB · Go
RawDownload
// Package http provides a registration interface for http servicespackage http import (	"bytes"	"context"	"crypto/tls"	"crypto/x509"	"errors"	"fmt"	"html/template"	"net"	"net/http"	"os"	"path/filepath"	"strings"	"sync"	"time" 	"github.com/go-chi/chi/v5"	"github.com/rclone/rclone/fs"	"github.com/rclone/rclone/fs/config/flags"	"github.com/rclone/rclone/lib/atexit"	sdActivation "github.com/rclone/rclone/lib/sdactivation"	"github.com/spf13/pflag") // Help returns text describing the http server to add to the command// help.func Help(prefix string) string {	help := `### Server options Use ` + "`--{{ .Prefix }}addr`" + ` to specify which IP address and port the server shouldlisten on, eg ` + "`--{{ .Prefix }}addr 1.2.3.4:8000` or `--{{ .Prefix }}addr :8080`" + ` to listen to allIPs.  By default it only listens on localhost.  You can use port:0 to let the OS choose an available port. If you set ` + "`--{{ .Prefix }}addr`" + ` to listen on a public or LAN accessible IP addressthen using Authentication is advised - see the next section for info. You can use a unix socket by setting the url to ` + "`unix:///path/to/socket`" + `or just by using an absolute path name. ` + "`--{{ .Prefix }}addr`" + ` may be repeated to listen on multiple IPs/ports/sockets.Socket activation, described further below, can also be used to accomplish the same. ` + "`--{{ .Prefix }}server-read-timeout` and `--{{ .Prefix }}server-write-timeout`" + ` can be used tocontrol the timeouts on the server.  Note that this is the total timefor a transfer. ` + "`--{{ .Prefix }}max-header-bytes`" + ` controls the maximum number of bytes the server willaccept in the HTTP header. ` + "`--{{ .Prefix }}baseurl`" + ` controls the URL prefix that rclone serves from.  By defaultrclone will serve from the root.  If you used ` + "`--{{ .Prefix }}baseurl \"/rclone\"`" + ` thenrclone would serve from a URL starting with "/rclone/".  This isuseful if you wish to proxy rclone serve.  Rclone automaticallyinserts leading and trailing "/" on ` + "`--{{ .Prefix }}baseurl`" + `, so ` + "`--{{ .Prefix }}baseurl \"rclone\"`" + `,` + "`--{{ .Prefix }}baseurl \"/rclone\"` and `--{{ .Prefix }}baseurl \"/rclone/\"`" + ` are all treatedidentically. #### TLS (SSL) By default this will serve over http.  If you want you can serve overhttps.  You will need to supply the ` + "`--{{ .Prefix }}cert` and `--{{ .Prefix }}key`" + ` flags.If you wish to do client side certificate validation then you will need tosupply ` + "`--{{ .Prefix }}client-ca`" + ` also. ` + "`--{{ .Prefix }}cert`" + ` must be set to the path of a file containingeither a PEM encoded certificate, or a concatenation of that with the CAcertificate. ` + "`--{{ .Prefix }}key`" + ` must be set to the path of a filewith the PEM encoded private key. ` + "If setting `--{{ .Prefix }}client-ca`" + `,it should be set to the path of a file with PEM encoded client certificateauthority certificates. ` + "`--{{ .Prefix }}min-tls-version`" + ` is minimum TLS version that is acceptable. Validvalues are "tls1.0", "tls1.1", "tls1.2" and "tls1.3" (default "tls1.0"). ### Socket activation Instead of the listening addresses specified above, rclone will listen to allFDs passed by the service manager, if any (and ignore any arguments passedby ` + "`--{{ .Prefix }}addr`" + `). This allows rclone to be a socket-activated service.It can be configured with .socket and .service unit files as described inhttps://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html Socket activation can be tested ad-hoc with the ` + "`systemd-socket-activate`" + `command        systemd-socket-activate -l 8000 -- rclone serve This will socket-activate rclone on the first connection to port 8000 over TCP.`	tmpl, err := template.New("server help").Parse(help)	if err != nil {		fs.Fatal(nil, fmt.Sprint("Fatal error parsing template", err))	} 	data := struct {		Prefix string	}{		Prefix: prefix,	}	buf := &bytes.Buffer{}	err = tmpl.Execute(buf, data)	if err != nil {		fs.Fatal(nil, fmt.Sprint("Fatal error executing template", err))	}	return buf.String()} // Middleware function signature required by chi.Router.Use()type Middleware func(http.Handler) http.Handler // ConfigInfo descripts the Options in usevar ConfigInfo = fs.Options{{	Name:    "addr",	Default: []string{"127.0.0.1:8080"},	Help:    "IPaddress:Port or :Port to bind server to",}, {	Name:    "server_read_timeout",	Default: fs.Duration(1 * time.Hour),	Help:    "Timeout for server reading data",}, {	Name:    "server_write_timeout",	Default: fs.Duration(1 * time.Hour),	Help:    "Timeout for server writing data",}, {	Name:    "max_header_bytes",	Default: 4096,	Help:    "Maximum size of request header",}, {	Name:    "cert",	Default: "",	Help:    "TLS PEM key (concatenation of certificate and CA certificate)",}, {	Name:    "key",	Default: "",	Help:    "TLS PEM Private key",}, {	Name:    "client_ca",	Default: "",	Help:    "Client certificate authority to verify clients with",}, {	Name:    "baseurl",	Default: "",	Help:    "Prefix for URLs - leave blank for root",}, {	Name:    "min_tls_version",	Default: "tls1.0",	Help:    "Minimum TLS version that is acceptable",}, {	Name:    "allow_origin",	Default: "",	Help:    "Origin which cross-domain request (CORS) can be executed from",}} // Config contains options for the http Servertype Config struct {	ListenAddr         []string    `config:"addr"`                 // Port to listen on	BaseURL            string      `config:"baseurl"`              // prefix to strip from URLs	ServerReadTimeout  fs.Duration `config:"server_read_timeout"`  // Timeout for server reading data	ServerWriteTimeout fs.Duration `config:"server_write_timeout"` // Timeout for server writing data	MaxHeaderBytes     int         `config:"max_header_bytes"`     // Maximum size of request header	TLSCert            string      `config:"cert"`                 // Path to TLS PEM public key certificate file (can also include intermediate/CA certificates)	TLSKey             string      `config:"key"`                  // Path to TLS PEM private key file	TLSCertBody        []byte      `config:"-"`                    // TLS PEM public key certificate body (can also include intermediate/CA certificates), ignores TLSCert	TLSKeyBody         []byte      `config:"-"`                    // TLS PEM private key body, ignores TLSKey	ClientCA           string      `config:"client_ca"`            // Path to TLS PEM CA file with certificate authorities to verify clients with	MinTLSVersion      string      `config:"min_tls_version"`      // MinTLSVersion contains the minimum TLS version that is acceptable.	AllowOrigin        string      `config:"allow_origin"`         // AllowOrigin sets the Access-Control-Allow-Origin header} // AddFlagsPrefix adds flags for the httplibfunc (cfg *Config) AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string) {	flags.StringArrayVarP(flagSet, &cfg.ListenAddr, prefix+"addr", "", cfg.ListenAddr, "IPaddress:Port, :Port or [unix://]/path/to/socket to bind server to", prefix)	flags.FVarP(flagSet, &cfg.ServerReadTimeout, prefix+"server-read-timeout", "", "Timeout for server reading data", prefix)	flags.FVarP(flagSet, &cfg.ServerWriteTimeout, prefix+"server-write-timeout", "", "Timeout for server writing data", prefix)	flags.IntVarP(flagSet, &cfg.MaxHeaderBytes, prefix+"max-header-bytes", "", cfg.MaxHeaderBytes, "Maximum size of request header", prefix)	flags.StringVarP(flagSet, &cfg.TLSCert, prefix+"cert", "", cfg.TLSCert, "Path to TLS PEM public key certificate file (can also include intermediate/CA certificates)", prefix)	flags.StringVarP(flagSet, &cfg.TLSKey, prefix+"key", "", cfg.TLSKey, "Path to TLS PEM private key file", prefix)	flags.StringVarP(flagSet, &cfg.ClientCA, prefix+"client-ca", "", cfg.ClientCA, "Path to TLS PEM CA file with certificate authorities to verify clients with", prefix)	flags.StringVarP(flagSet, &cfg.BaseURL, prefix+"baseurl", "", cfg.BaseURL, "Prefix for URLs - leave blank for root", prefix)	flags.StringVarP(flagSet, &cfg.MinTLSVersion, prefix+"min-tls-version", "", cfg.MinTLSVersion, "Minimum TLS version that is acceptable", prefix)	flags.StringVarP(flagSet, &cfg.AllowOrigin, prefix+"allow-origin", "", cfg.AllowOrigin, "Origin which cross-domain request (CORS) can be executed from", prefix)} // AddHTTPFlagsPrefix adds flags for the httplibfunc AddHTTPFlagsPrefix(flagSet *pflag.FlagSet, prefix string, cfg *Config) {	cfg.AddFlagsPrefix(flagSet, prefix)} // DefaultCfg is the default values used for Config//// Note that this needs to be kept in sync with ConfigInfo above and// can be removed when all callers have been converted.func DefaultCfg() Config {	return Config{		ListenAddr:         []string{"127.0.0.1:8080"},		ServerReadTimeout:  fs.Duration(1 * time.Hour),		ServerWriteTimeout: fs.Duration(1 * time.Hour),		MaxHeaderBytes:     4096,		MinTLSVersion:      "tls1.0",	}} type instance struct {	url        string	listener   net.Listener	httpServer *http.Server} func (s instance) serve(wg *sync.WaitGroup) {	defer wg.Done()	err := s.httpServer.Serve(s.listener)	if err != http.ErrServerClosed && err != nil {		fs.Logf(nil, "%s: unexpected error: %s", s.listener.Addr(), err.Error())	}} // Server contains info about the running http servertype Server struct {	wg           sync.WaitGroup	mux          chi.Router	tlsConfig    *tls.Config	instances    []instance	auth         AuthConfig	cfg          Config	template     *TemplateConfig	htmlTemplate *template.Template	usingAuth    bool       // set if we are using auth middleware	mu           sync.Mutex // mutex protects RW variables below	atexitHandle atexit.FnHandle} // Option allows customizing the servertype Option func(*Server) // WithAuth option initializes the appropriate auth middlewarefunc WithAuth(cfg AuthConfig) Option {	return func(s *Server) {		s.auth = cfg	}} // WithConfig option applies the Config to the server, overriding defaultsfunc WithConfig(cfg Config) Option {	return func(s *Server) {		s.cfg = cfg	}} // WithTemplate option allows the parsing of a templatefunc WithTemplate(cfg TemplateConfig) Option {	return func(s *Server) {		s.template = &cfg	}} // For a given listener, and optional tlsConfig, construct a instance.// The url string ends up in the `url` field of the `instance`.// This unconditionally wraps the listener with the provided TLS config if one// is specified, so all decision logic on whether to use TLS needs to live at// the callsite.func newInstance(ctx context.Context, s *Server, listener net.Listener, tlsCfg *tls.Config, url string) *instance {	if tlsCfg != nil {		listener = tls.NewListener(listener, tlsCfg)	} 	return &instance{		url:      url,		listener: listener,		httpServer: &http.Server{			Handler:           s.mux,			ReadTimeout:       time.Duration(s.cfg.ServerReadTimeout),			WriteTimeout:      time.Duration(s.cfg.ServerWriteTimeout),			MaxHeaderBytes:    s.cfg.MaxHeaderBytes,			ReadHeaderTimeout: 10 * time.Second, // time to send the headers			IdleTimeout:       60 * time.Second, // time to keep idle connections open			TLSConfig:         tlsCfg,			BaseContext:       NewBaseContext(ctx, url),		},	}} // NewServer instantiates a new http server using provided listeners and options// This function is provided if the default http server does not meet a services requirements and should not generally be used// A http server can listen using multiple listeners. For example, a listener for port 80, and a listener for port 443.// tlsListeners are ignored if opt.TLSKey is not providedfunc NewServer(ctx context.Context, options ...Option) (*Server, error) {	s := &Server{		mux: chi.NewRouter(),		cfg: DefaultCfg(),	} 	// Make sure default logger is logging where everything else is	// middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: log.Default(), NoColor: true})	// Log requests	// s.mux.Use(middleware.Logger) 	for _, opt := range options {		opt(s)	} 	// Build base router	s.mux.MethodNotAllowed(func(w http.ResponseWriter, _ *http.Request) {		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)	})	s.mux.NotFound(func(w http.ResponseWriter, _ *http.Request) {		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)	}) 	// Ignore passing "/" for BaseURL	s.cfg.BaseURL = strings.Trim(s.cfg.BaseURL, "/")	if s.cfg.BaseURL != "" {		s.cfg.BaseURL = "/" + s.cfg.BaseURL		s.mux.Use(MiddlewareStripPrefix(s.cfg.BaseURL))	} 	err := s.initTemplate()	if err != nil {		return nil, err	} 	err = s.initTLS()	if err != nil {		return nil, err	} 	s.mux.Use(MiddlewareCORS(s.cfg.AllowOrigin)) 	s.initAuth() 	// (Only) listen on FDs provided by the service manager, if any.	sdListeners, err := sdActivation.ListenersWithNames()	if err != nil {		return nil, fmt.Errorf("unable to acquire listeners: %w", err)	} 	if len(sdListeners) != 0 {		for listenerName, listeners := range sdListeners {			for i, listener := range listeners {				url := fmt.Sprintf("sd-listen:%s-%d/%s", listenerName, i, s.cfg.BaseURL)				if s.tlsConfig != nil {					url = fmt.Sprintf("sd-listen+tls:%s-%d/%s", listenerName, i, s.cfg.BaseURL)				} 				instance := newInstance(ctx, s, listener, s.tlsConfig, url) 				s.instances = append(s.instances, *instance)			}		} 		return s, nil	} 	// Process all listeners specified in the CLI Args.	for _, addr := range s.cfg.ListenAddr {		var instance *instance 		if strings.HasPrefix(addr, "unix://") || filepath.IsAbs(addr) {			addr = strings.TrimPrefix(addr, "unix://") 			listener, err := net.Listen("unix", addr)			if err != nil {				return nil, err			}			instance = newInstance(ctx, s, listener, s.tlsConfig, addr)		} else if strings.HasPrefix(addr, "tls://") || (len(s.cfg.ListenAddr) == 1 && s.tlsConfig != nil) {			addr = strings.TrimPrefix(addr, "tls://")			listener, err := net.Listen("tcp", addr)			if err != nil {				return nil, err			}			instance = newInstance(ctx, s, listener, s.tlsConfig, fmt.Sprintf("https://%s%s/", listener.Addr().String(), s.cfg.BaseURL))		} else {			// HTTP case			addr = strings.TrimPrefix(addr, "http://")			listener, err := net.Listen("tcp", addr)			if err != nil {				return nil, err			}			instance = newInstance(ctx, s, listener, nil, fmt.Sprintf("http://%s%s/", listener.Addr().String(), s.cfg.BaseURL)) 		} 		s.instances = append(s.instances, *instance)	} 	return s, nil} func (s *Server) initAuth() {	s.usingAuth = false	altUsernameEnabled := s.auth.HtPasswd == "" && s.auth.BasicUser == "" 	if altUsernameEnabled {		s.usingAuth = true		if s.auth.UserFromHeader != "" {			s.mux.Use(MiddlewareAuthGetUserFromHeader(s.auth.UserFromHeader))		} else if s.tlsConfig != nil && s.tlsConfig.ClientAuth != tls.NoClientCert {			s.mux.Use(MiddlewareAuthCertificateUser())		} else {			s.usingAuth = false			altUsernameEnabled = false		}	} 	if s.auth.CustomAuthFn != nil {		s.usingAuth = true		s.mux.Use(MiddlewareAuthCustom(s.auth.CustomAuthFn, s.auth.Realm, altUsernameEnabled))		return	} 	if s.auth.HtPasswd != "" {		s.usingAuth = true		s.mux.Use(MiddlewareAuthHtpasswd(s.auth.HtPasswd, s.auth.Realm))		return	} 	if s.auth.BasicUser != "" {		s.usingAuth = true		s.mux.Use(MiddlewareAuthBasic(s.auth.BasicUser, s.auth.BasicPass, s.auth.Realm, s.auth.Salt))		return	}} func (s *Server) initTemplate() error {	if s.template == nil {		return nil	} 	var err error	s.htmlTemplate, err = GetTemplate(s.template.Path)	if err != nil {		err = fmt.Errorf("failed to get template: %w", err)	} 	return err} var (	// ErrInvalidMinTLSVersion - hard coded errors, allowing for easier testing	ErrInvalidMinTLSVersion = errors.New("invalid value for --min-tls-version")	// ErrTLSBodyMismatch - hard coded errors, allowing for easier testing	ErrTLSBodyMismatch = errors.New("need both TLSCertBody and TLSKeyBody to use TLS")	// ErrTLSFileMismatch - hard coded errors, allowing for easier testing	ErrTLSFileMismatch = errors.New("need both --cert and --key to use TLS")	// ErrTLSParseCA - hard coded errors, allowing for easier testing	ErrTLSParseCA = errors.New("unable to parse client certificate authority")) func (s *Server) initTLS() error {	if s.cfg.TLSCert == "" && s.cfg.TLSKey == "" && len(s.cfg.TLSCertBody) == 0 && len(s.cfg.TLSKeyBody) == 0 {		return nil	} 	if (len(s.cfg.TLSCertBody) > 0) != (len(s.cfg.TLSKeyBody) > 0) {		return ErrTLSBodyMismatch	} 	if (s.cfg.TLSCert != "") != (s.cfg.TLSKey != "") {		return ErrTLSFileMismatch	} 	var cert tls.Certificate	var err error	if len(s.cfg.TLSCertBody) > 0 {		cert, err = tls.X509KeyPair(s.cfg.TLSCertBody, s.cfg.TLSKeyBody)	} else {		cert, err = tls.LoadX509KeyPair(s.cfg.TLSCert, s.cfg.TLSKey)	}	if err != nil {		return err	} 	var minTLSVersion uint16	switch s.cfg.MinTLSVersion {	case "tls1.0":		minTLSVersion = tls.VersionTLS10	case "tls1.1":		minTLSVersion = tls.VersionTLS11	case "tls1.2":		minTLSVersion = tls.VersionTLS12	case "tls1.3":		minTLSVersion = tls.VersionTLS13	default:		return fmt.Errorf("%w: %s", ErrInvalidMinTLSVersion, s.cfg.MinTLSVersion)	} 	s.tlsConfig = &tls.Config{		MinVersion:   minTLSVersion,		Certificates: []tls.Certificate{cert},	} 	if s.cfg.ClientCA != "" {		// if !useTLS {		// 	err := errors.New("can't use --client-ca without --cert and --key")		// 	log.Fatalf(err.Error())

Lines 1–500 of 595.