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 }