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