github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/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/json" 6 "mime" 7 "net/http" 8 "net/url" 9 "regexp" 10 "sort" 11 "strings" 12 13 "github.com/ncw/rclone/cmd/serve/httplib" 14 "github.com/ncw/rclone/cmd/serve/httplib/serve" 15 "github.com/ncw/rclone/fs" 16 "github.com/ncw/rclone/fs/cache" 17 "github.com/ncw/rclone/fs/config" 18 "github.com/ncw/rclone/fs/list" 19 "github.com/ncw/rclone/fs/rc" 20 "github.com/pkg/errors" 21 "github.com/skratchdot/open-golang/open" 22 ) 23 24 // Start the remote control server if configured 25 // 26 // If the server wasn't configured the *Server returned may be nil 27 func Start(opt *rc.Options) (*Server, error) { 28 if opt.Enabled { 29 // Serve on the DefaultServeMux so can have global registrations appear 30 s := newServer(opt, http.DefaultServeMux) 31 return s, s.Serve() 32 } 33 return nil, nil 34 } 35 36 // Server contains everything to run the rc server 37 type Server struct { 38 *httplib.Server 39 files http.Handler 40 opt *rc.Options 41 } 42 43 func newServer(opt *rc.Options, mux *http.ServeMux) *Server { 44 s := &Server{ 45 Server: httplib.NewServer(mux, &opt.HTTPOptions), 46 opt: opt, 47 } 48 mux.HandleFunc("/", s.handler) 49 50 // Add some more mime types which are often missing 51 _ = mime.AddExtensionType(".wasm", "application/wasm") 52 _ = mime.AddExtensionType(".js", "application/javascript") 53 54 // File handling 55 if opt.Files != "" { 56 fs.Logf(nil, "Serving files from %q", opt.Files) 57 s.files = http.FileServer(http.Dir(opt.Files)) 58 } 59 return s 60 } 61 62 // Serve runs the http server in the background. 63 // 64 // Use s.Close() and s.Wait() to shutdown server 65 func (s *Server) Serve() error { 66 err := s.Server.Serve() 67 if err != nil { 68 return err 69 } 70 fs.Logf(nil, "Serving remote control on %s", s.URL()) 71 // Open the files in the browser if set 72 if s.files != nil { 73 openURL, err := url.Parse(s.URL()) 74 if err != nil { 75 return errors.Wrap(err, "invalid serving URL") 76 } 77 // Add username, password into the URL if they are set 78 user, pass := s.opt.HTTPOptions.BasicUser, s.opt.HTTPOptions.BasicPass 79 if user != "" || pass != "" { 80 openURL.User = url.UserPassword(user, pass) 81 } 82 _ = open.Start(openURL.String()) 83 } 84 return nil 85 } 86 87 // writeError writes a formatted error to the output 88 func writeError(path string, in rc.Params, w http.ResponseWriter, err error, status int) { 89 fs.Errorf(nil, "rc: %q: error: %v", path, err) 90 // Adjust the error return for some well known errors 91 errOrig := errors.Cause(err) 92 switch { 93 case errOrig == fs.ErrorDirNotFound || errOrig == fs.ErrorObjectNotFound: 94 status = http.StatusNotFound 95 case rc.IsErrParamInvalid(err) || rc.IsErrParamNotFound(err): 96 status = http.StatusBadRequest 97 } 98 w.WriteHeader(status) 99 err = rc.WriteJSON(w, rc.Params{ 100 "status": status, 101 "error": err.Error(), 102 "input": in, 103 "path": path, 104 }) 105 if err != nil { 106 // can't return the error at this point 107 fs.Errorf(nil, "rc: failed to write JSON output: %v", err) 108 } 109 } 110 111 // handler reads incoming requests and dispatches them 112 func (s *Server) handler(w http.ResponseWriter, r *http.Request) { 113 path := strings.TrimLeft(r.URL.Path, "/") 114 115 w.Header().Add("Access-Control-Allow-Origin", "*") 116 117 // echo back access control headers client needs 118 reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers") 119 w.Header().Add("Access-Control-Allow-Headers", reqAccessHeaders) 120 121 switch r.Method { 122 case "POST": 123 s.handlePost(w, r, path) 124 case "OPTIONS": 125 s.handleOptions(w, r, path) 126 case "GET", "HEAD": 127 s.handleGet(w, r, path) 128 default: 129 writeError(path, nil, w, errors.Errorf("method %q not allowed", r.Method), http.StatusMethodNotAllowed) 130 return 131 } 132 } 133 134 func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string) { 135 contentType := r.Header.Get("Content-Type") 136 137 values := r.URL.Query() 138 if contentType == "application/x-www-form-urlencoded" { 139 // Parse the POST and URL parameters into r.Form, for others r.Form will be empty value 140 err := r.ParseForm() 141 if err != nil { 142 writeError(path, nil, w, errors.Wrap(err, "failed to parse form/URL parameters"), http.StatusBadRequest) 143 return 144 } 145 values = r.Form 146 } 147 148 // Read the POST and URL parameters into in 149 in := make(rc.Params) 150 for k, vs := range values { 151 if len(vs) > 0 { 152 in[k] = vs[len(vs)-1] 153 } 154 } 155 156 // Parse a JSON blob from the input 157 if contentType == "application/json" { 158 err := json.NewDecoder(r.Body).Decode(&in) 159 if err != nil { 160 writeError(path, in, w, errors.Wrap(err, "failed to read input JSON"), http.StatusBadRequest) 161 return 162 } 163 } 164 165 // Find the call 166 call := rc.Calls.Get(path) 167 if call == nil { 168 writeError(path, in, w, errors.Errorf("couldn't find method %q", path), http.StatusNotFound) 169 return 170 } 171 172 // Check to see if it requires authorisation 173 if !s.opt.NoAuth && call.AuthRequired && !s.UsingAuth() { 174 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) 175 return 176 } 177 178 // Check to see if it is async or not 179 isAsync, err := in.GetBool("_async") 180 if rc.NotErrParamNotFound(err) { 181 writeError(path, in, w, err, http.StatusBadRequest) 182 return 183 } 184 185 delete(in, "_async") // remove the async parameter after parsing so vfs operations don't get confused 186 187 fs.Debugf(nil, "rc: %q: with parameters %+v", path, in) 188 var out rc.Params 189 if isAsync { 190 out, err = rc.StartJob(call.Fn, in) 191 } else { 192 out, err = call.Fn(r.Context(), in) 193 } 194 if err != nil { 195 writeError(path, in, w, err, http.StatusInternalServerError) 196 return 197 } 198 if out == nil { 199 out = make(rc.Params) 200 } 201 202 fs.Debugf(nil, "rc: %q: reply %+v: %v", path, out, err) 203 err = rc.WriteJSON(w, out) 204 if err != nil { 205 // can't return the error at this point 206 fs.Errorf(nil, "rc: failed to write JSON output: %v", err) 207 } 208 } 209 210 func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request, path string) { 211 w.WriteHeader(http.StatusOK) 212 } 213 214 func (s *Server) serveRoot(w http.ResponseWriter, r *http.Request) { 215 remotes := config.FileSections() 216 sort.Strings(remotes) 217 directory := serve.NewDirectory("", s.HTMLTemplate) 218 directory.Title = "List of all rclone remotes." 219 q := url.Values{} 220 for _, remote := range remotes { 221 q.Set("fs", remote) 222 directory.AddEntry("["+remote+":]", true) 223 } 224 directory.Serve(w, r) 225 } 226 227 func (s *Server) serveRemote(w http.ResponseWriter, r *http.Request, path string, fsName string) { 228 f, err := cache.Get(fsName) 229 if err != nil { 230 writeError(path, nil, w, errors.Wrap(err, "failed to make Fs"), http.StatusInternalServerError) 231 return 232 } 233 if path == "" || strings.HasSuffix(path, "/") { 234 path = strings.Trim(path, "/") 235 entries, err := list.DirSorted(r.Context(), f, false, path) 236 if err != nil { 237 writeError(path, nil, w, errors.Wrap(err, "failed to list directory"), http.StatusInternalServerError) 238 return 239 } 240 // Make the entries for display 241 directory := serve.NewDirectory(path, s.HTMLTemplate) 242 for _, entry := range entries { 243 _, isDir := entry.(fs.Directory) 244 directory.AddEntry(entry.Remote(), isDir) 245 } 246 directory.Serve(w, r) 247 } else { 248 path = strings.Trim(path, "/") 249 o, err := f.NewObject(r.Context(), path) 250 if err != nil { 251 writeError(path, nil, w, errors.Wrap(err, "failed to find object"), http.StatusInternalServerError) 252 return 253 } 254 serve.Object(w, r, o) 255 } 256 } 257 258 // Match URLS of the form [fs]/remote 259 var fsMatch = regexp.MustCompile(`^\[(.*?)\](.*)$`) 260 261 func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, path string) { 262 // Look to see if this has an fs in the path 263 match := fsMatch.FindStringSubmatch(path) 264 switch { 265 case match != nil && s.opt.Serve: 266 // Serve /[fs]/remote files 267 s.serveRemote(w, r, match[2], match[1]) 268 return 269 case path == "*" && s.opt.Serve: 270 // Serve /* as the remote listing 271 s.serveRoot(w, r) 272 return 273 case s.files != nil: 274 // Serve the files 275 s.files.ServeHTTP(w, r) 276 return 277 case path == "" && s.opt.Serve: 278 // Serve the root as a remote listing 279 s.serveRoot(w, r) 280 return 281 } 282 http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 283 }