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