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 }