github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/rcserver/rcserver.go (about)

     1  // Package rcserver implements the HTTP endpoint to serve the remote control
     2  package rcserver
     3  
     4  import (
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"flag"
     9  	"fmt"
    10  	"log"
    11  	"mime"
    12  	"net/http"
    13  	"net/url"
    14  	"path/filepath"
    15  	"regexp"
    16  	"sort"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/go-chi/chi/v5/middleware"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  	"github.com/prometheus/client_golang/prometheus/promhttp"
    23  	"github.com/rclone/rclone/fs"
    24  	"github.com/rclone/rclone/fs/accounting"
    25  	"github.com/rclone/rclone/fs/cache"
    26  	"github.com/rclone/rclone/fs/config"
    27  	"github.com/rclone/rclone/fs/fshttp"
    28  	"github.com/rclone/rclone/fs/list"
    29  	"github.com/rclone/rclone/fs/rc"
    30  	"github.com/rclone/rclone/fs/rc/jobs"
    31  	"github.com/rclone/rclone/fs/rc/rcflags"
    32  	"github.com/rclone/rclone/fs/rc/webgui"
    33  	libhttp "github.com/rclone/rclone/lib/http"
    34  	"github.com/rclone/rclone/lib/http/serve"
    35  	"github.com/rclone/rclone/lib/random"
    36  	"github.com/skratchdot/open-golang/open"
    37  )
    38  
    39  var promHandler http.Handler
    40  
    41  func init() {
    42  	rcloneCollector := accounting.NewRcloneCollector(context.Background())
    43  	prometheus.MustRegister(rcloneCollector)
    44  
    45  	m := fshttp.NewMetrics("rclone")
    46  	for _, c := range m.Collectors() {
    47  		prometheus.MustRegister(c)
    48  	}
    49  	fshttp.DefaultMetrics = m
    50  
    51  	promHandler = promhttp.Handler()
    52  }
    53  
    54  // Start the remote control server if configured
    55  //
    56  // If the server wasn't configured the *Server returned may be nil
    57  func Start(ctx context.Context, opt *rc.Options) (*Server, error) {
    58  	jobs.SetOpt(opt) // set the defaults for jobs
    59  	if opt.Enabled {
    60  		// Serve on the DefaultServeMux so can have global registrations appear
    61  		s, err := newServer(ctx, opt, http.DefaultServeMux)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  		return s, s.Serve()
    66  	}
    67  	return nil, nil
    68  }
    69  
    70  // Server contains everything to run the rc server
    71  type Server struct {
    72  	ctx            context.Context // for global config
    73  	server         *libhttp.Server
    74  	files          http.Handler
    75  	pluginsHandler http.Handler
    76  	opt            *rc.Options
    77  }
    78  
    79  func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) (*Server, error) {
    80  	fileHandler := http.Handler(nil)
    81  	pluginsHandler := http.Handler(nil)
    82  	// Add some more mime types which are often missing
    83  	_ = mime.AddExtensionType(".wasm", "application/wasm")
    84  	_ = mime.AddExtensionType(".js", "application/javascript")
    85  
    86  	cachePath := filepath.Join(config.GetCacheDir(), "webgui")
    87  	extractPath := filepath.Join(cachePath, "current/build")
    88  	// File handling
    89  	if opt.Files != "" {
    90  		if opt.WebUI {
    91  			fs.Logf(nil, "--rc-files overrides --rc-web-gui command\n")
    92  		}
    93  		fs.Logf(nil, "Serving files from %q", opt.Files)
    94  		fileHandler = http.FileServer(http.Dir(opt.Files))
    95  	} else if opt.WebUI {
    96  		if err := webgui.CheckAndDownloadWebGUIRelease(opt.WebGUIUpdate, opt.WebGUIForceUpdate, opt.WebGUIFetchURL, config.GetCacheDir()); err != nil {
    97  			fs.Errorf(nil, "Error while fetching the latest release of Web GUI: %v", err)
    98  		}
    99  		if opt.NoAuth {
   100  			fs.Logf(nil, "It is recommended to use web gui with auth.")
   101  		} else {
   102  			if opt.Auth.BasicUser == "" && opt.Auth.HtPasswd == "" {
   103  				opt.Auth.BasicUser = "gui"
   104  				fs.Infof(nil, "No username specified. Using default username: %s \n", rcflags.Opt.Auth.BasicUser)
   105  			}
   106  			if opt.Auth.BasicPass == "" && opt.Auth.HtPasswd == "" {
   107  				randomPass, err := random.Password(128)
   108  				if err != nil {
   109  					log.Fatalf("Failed to make password: %v", err)
   110  				}
   111  				opt.Auth.BasicPass = randomPass
   112  				fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
   113  			}
   114  		}
   115  		opt.Serve = true
   116  
   117  		fs.Logf(nil, "Serving Web GUI")
   118  		fileHandler = http.FileServer(http.Dir(extractPath))
   119  
   120  		pluginsHandler = http.FileServer(http.Dir(webgui.PluginsPath))
   121  	}
   122  
   123  	s := &Server{
   124  		ctx:            ctx,
   125  		opt:            opt,
   126  		files:          fileHandler,
   127  		pluginsHandler: pluginsHandler,
   128  	}
   129  
   130  	var err error
   131  	s.server, err = libhttp.NewServer(ctx,
   132  		libhttp.WithConfig(opt.HTTP),
   133  		libhttp.WithAuth(opt.Auth),
   134  		libhttp.WithTemplate(opt.Template),
   135  	)
   136  	if err != nil {
   137  		return nil, fmt.Errorf("failed to init server: %w", err)
   138  	}
   139  
   140  	router := s.server.Router()
   141  	router.Use(
   142  		middleware.SetHeader("Accept-Ranges", "bytes"),
   143  		middleware.SetHeader("Server", "rclone/"+fs.Version),
   144  	)
   145  
   146  	// Add the debug handler which is installed in the default mux
   147  	router.Handle("/debug/*", mux)
   148  
   149  	// FIXME split these up into individual functions
   150  	router.Get("/*", s.handler)
   151  	router.Head("/*", s.handler)
   152  	router.Post("/*", s.handler)
   153  	router.Options("/*", s.handler)
   154  
   155  	return s, nil
   156  }
   157  
   158  // Serve runs the http server in the background.
   159  //
   160  // Use s.Close() and s.Wait() to shutdown server
   161  func (s *Server) Serve() error {
   162  	s.server.Serve()
   163  
   164  	for _, URL := range s.server.URLs() {
   165  		fs.Logf(nil, "Serving remote control on %s", URL)
   166  		// Open the files in the browser if set
   167  		if s.files != nil {
   168  			openURL, err := url.Parse(URL)
   169  			if err != nil {
   170  				return fmt.Errorf("invalid serving URL: %w", err)
   171  			}
   172  			// Add username, password into the URL if they are set
   173  			user, pass := s.opt.Auth.BasicUser, s.opt.Auth.BasicPass
   174  			if user != "" && pass != "" {
   175  				openURL.User = url.UserPassword(user, pass)
   176  
   177  				// Base64 encode username and password to be sent through url
   178  				loginToken := user + ":" + pass
   179  				parameters := url.Values{}
   180  				encodedToken := base64.URLEncoding.EncodeToString([]byte(loginToken))
   181  				fs.Debugf(nil, "login_token %q", encodedToken)
   182  				parameters.Add("login_token", encodedToken)
   183  				openURL.RawQuery = parameters.Encode()
   184  				openURL.RawPath = "/#/login"
   185  			}
   186  			// Don't open browser if serving in testing environment or required not to do so.
   187  			if flag.Lookup("test.v") == nil && !s.opt.WebGUINoOpenBrowser {
   188  				if err := open.Start(openURL.String()); err != nil {
   189  					fs.Errorf(nil, "Failed to open Web GUI in browser: %v. Manually access it at: %s", err, openURL.String())
   190  				}
   191  			} else {
   192  				fs.Logf(nil, "Web GUI is not automatically opening browser. Navigate to %s to use.", openURL.String())
   193  			}
   194  		}
   195  	}
   196  	return nil
   197  }
   198  
   199  // writeError writes a formatted error to the output
   200  func writeError(path string, in rc.Params, w http.ResponseWriter, err error, status int) {
   201  	fs.Errorf(nil, "rc: %q: error: %v", path, err)
   202  	params, status := rc.Error(path, in, err, status)
   203  	w.Header().Set("Content-Type", "application/json")
   204  	w.WriteHeader(status)
   205  	err = rc.WriteJSON(w, params)
   206  	if err != nil {
   207  		// can't return the error at this point
   208  		fs.Errorf(nil, "rc: writeError: failed to write JSON output from %#v: %v", in, err)
   209  	}
   210  }
   211  
   212  // handler reads incoming requests and dispatches them
   213  func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
   214  	path := strings.TrimLeft(r.URL.Path, "/")
   215  
   216  	switch r.Method {
   217  	case "POST":
   218  		s.handlePost(w, r, path)
   219  	case "OPTIONS":
   220  		s.handleOptions(w, r, path)
   221  	case "GET", "HEAD":
   222  		s.handleGet(w, r, path)
   223  	default:
   224  		writeError(path, nil, w, fmt.Errorf("method %q not allowed", r.Method), http.StatusMethodNotAllowed)
   225  		return
   226  	}
   227  }
   228  
   229  func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string) {
   230  	ctx := r.Context()
   231  	contentType := r.Header.Get("Content-Type")
   232  
   233  	values := r.URL.Query()
   234  	if contentType == "application/x-www-form-urlencoded" {
   235  		// Parse the POST and URL parameters into r.Form, for others r.Form will be empty value
   236  		err := r.ParseForm()
   237  		if err != nil {
   238  			writeError(path, nil, w, fmt.Errorf("failed to parse form/URL parameters: %w", err), http.StatusBadRequest)
   239  			return
   240  		}
   241  		values = r.Form
   242  	}
   243  
   244  	// Read the POST and URL parameters into in
   245  	in := make(rc.Params)
   246  	for k, vs := range values {
   247  		if len(vs) > 0 {
   248  			in[k] = vs[len(vs)-1]
   249  		}
   250  	}
   251  
   252  	// Parse a JSON blob from the input
   253  	if contentType == "application/json" {
   254  		err := json.NewDecoder(r.Body).Decode(&in)
   255  		if err != nil {
   256  			writeError(path, in, w, fmt.Errorf("failed to read input JSON: %w", err), http.StatusBadRequest)
   257  			return
   258  		}
   259  	}
   260  	// Find the call
   261  	call := rc.Calls.Get(path)
   262  	if call == nil {
   263  		writeError(path, in, w, fmt.Errorf("couldn't find method %q", path), http.StatusNotFound)
   264  		return
   265  	}
   266  
   267  	// Check to see if it requires authorisation
   268  	if !s.opt.NoAuth && call.AuthRequired && !s.server.UsingAuth() {
   269  		writeError(path, in, w, fmt.Errorf("authentication must be set up on the rc server to use %q or the --rc-no-auth flag must be in use", path), http.StatusForbidden)
   270  		return
   271  	}
   272  
   273  	inOrig := in.Copy()
   274  
   275  	if call.NeedsRequest {
   276  		// Add the request to RC
   277  		in["_request"] = r
   278  	}
   279  
   280  	if call.NeedsResponse {
   281  		in["_response"] = w
   282  	}
   283  
   284  	fs.Debugf(nil, "rc: %q: with parameters %+v", path, in)
   285  	job, out, err := jobs.NewJob(ctx, call.Fn, in)
   286  	if job != nil {
   287  		w.Header().Add("x-rclone-jobid", fmt.Sprintf("%d", job.ID))
   288  	}
   289  	if err != nil {
   290  		writeError(path, inOrig, w, err, http.StatusInternalServerError)
   291  		return
   292  	}
   293  	if out == nil {
   294  		out = make(rc.Params)
   295  	}
   296  
   297  	fs.Debugf(nil, "rc: %q: reply %+v: %v", path, out, err)
   298  	w.Header().Set("Content-Type", "application/json")
   299  	err = rc.WriteJSON(w, out)
   300  	if err != nil {
   301  		// can't return the error at this point - but have a go anyway
   302  		writeError(path, inOrig, w, err, http.StatusInternalServerError)
   303  		fs.Errorf(nil, "rc: handlePost: failed to write JSON output: %v", err)
   304  	}
   305  }
   306  
   307  func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request, path string) {
   308  	w.WriteHeader(http.StatusOK)
   309  }
   310  
   311  func (s *Server) serveRoot(w http.ResponseWriter, r *http.Request) {
   312  	remotes := config.FileSections()
   313  	sort.Strings(remotes)
   314  	directory := serve.NewDirectory("", s.server.HTMLTemplate())
   315  	directory.Name = "List of all rclone remotes."
   316  	q := url.Values{}
   317  	for _, remote := range remotes {
   318  		q.Set("fs", remote)
   319  		directory.AddHTMLEntry("["+remote+":]", true, -1, time.Time{})
   320  	}
   321  	sortParm := r.URL.Query().Get("sort")
   322  	orderParm := r.URL.Query().Get("order")
   323  	directory.ProcessQueryParams(sortParm, orderParm)
   324  
   325  	directory.Serve(w, r)
   326  }
   327  
   328  func (s *Server) serveRemote(w http.ResponseWriter, r *http.Request, path string, fsName string) {
   329  	f, err := cache.Get(s.ctx, fsName)
   330  	if err != nil {
   331  		writeError(path, nil, w, fmt.Errorf("failed to make Fs: %w", err), http.StatusInternalServerError)
   332  		return
   333  	}
   334  	if path == "" || strings.HasSuffix(path, "/") {
   335  		path = strings.Trim(path, "/")
   336  		entries, err := list.DirSorted(r.Context(), f, false, path)
   337  		if err != nil {
   338  			writeError(path, nil, w, fmt.Errorf("failed to list directory: %w", err), http.StatusInternalServerError)
   339  			return
   340  		}
   341  		// Make the entries for display
   342  		directory := serve.NewDirectory(path, s.server.HTMLTemplate())
   343  		for _, entry := range entries {
   344  			_, isDir := entry.(fs.Directory)
   345  			var modTime time.Time
   346  			if !s.opt.ServeNoModTime {
   347  				modTime = entry.ModTime(r.Context())
   348  			}
   349  			directory.AddHTMLEntry(entry.Remote(), isDir, entry.Size(), modTime)
   350  		}
   351  		sortParm := r.URL.Query().Get("sort")
   352  		orderParm := r.URL.Query().Get("order")
   353  		directory.ProcessQueryParams(sortParm, orderParm)
   354  
   355  		directory.Serve(w, r)
   356  	} else {
   357  		path = strings.Trim(path, "/")
   358  		o, err := f.NewObject(r.Context(), path)
   359  		if err != nil {
   360  			writeError(path, nil, w, fmt.Errorf("failed to find object: %w", err), http.StatusInternalServerError)
   361  			return
   362  		}
   363  		serve.Object(w, r, o)
   364  	}
   365  }
   366  
   367  // Match URLS of the form [fs]/remote
   368  var fsMatch = regexp.MustCompile(`^\[(.*?)\](.*)$`)
   369  
   370  func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, path string) {
   371  	// Look to see if this has an fs in the path
   372  	fsMatchResult := fsMatch.FindStringSubmatch(path)
   373  
   374  	switch {
   375  	case fsMatchResult != nil && s.opt.Serve:
   376  		// Serve /[fs]/remote files
   377  		s.serveRemote(w, r, fsMatchResult[2], fsMatchResult[1])
   378  		return
   379  	case path == "metrics" && s.opt.EnableMetrics:
   380  		promHandler.ServeHTTP(w, r)
   381  		return
   382  	case path == "*" && s.opt.Serve:
   383  		// Serve /* as the remote listing
   384  		s.serveRoot(w, r)
   385  		return
   386  	case s.files != nil:
   387  		if s.opt.WebUI {
   388  			pluginsMatchResult := webgui.PluginsMatch.FindStringSubmatch(path)
   389  
   390  			if len(pluginsMatchResult) > 2 {
   391  				ok := webgui.ServePluginOK(w, r, pluginsMatchResult)
   392  				if !ok {
   393  					r.URL.Path = fmt.Sprintf("/%s/%s/app/build/%s", pluginsMatchResult[1], pluginsMatchResult[2], pluginsMatchResult[3])
   394  					s.pluginsHandler.ServeHTTP(w, r)
   395  					return
   396  				}
   397  				return
   398  			} else if webgui.ServePluginWithReferrerOK(w, r, path) {
   399  				return
   400  			}
   401  		}
   402  		// Serve the files
   403  		r.URL.Path = "/" + path
   404  		s.files.ServeHTTP(w, r)
   405  		return
   406  	case path == "" && s.opt.Serve:
   407  		// Serve the root as a remote listing
   408  		s.serveRoot(w, r)
   409  		return
   410  	}
   411  	http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   412  }
   413  
   414  // Wait blocks while the server is serving requests
   415  func (s *Server) Wait() {
   416  	s.server.Wait()
   417  }
   418  
   419  // Shutdown gracefully shuts down the server
   420  func (s *Server) Shutdown() error {
   421  	return s.server.Shutdown()
   422  }