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 }