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