2018-10-27 17:29:20 +00:00
// Package rcserver implements the HTTP endpoint to serve the remote control
package rcserver
import (
2020-11-05 15:18:51 +00:00
"context"
2019-08-20 18:25:04 +00:00
"encoding/base64"
2018-10-27 17:29:20 +00:00
"encoding/json"
2019-07-18 10:13:54 +00:00
"flag"
"fmt"
2020-01-12 09:12:04 +00:00
"log"
2018-10-27 17:29:20 +00:00
"mime"
"net/http"
2018-10-28 14:31:24 +00:00
"net/url"
2019-08-04 11:32:37 +00:00
"path/filepath"
2018-10-28 14:31:24 +00:00
"regexp"
"sort"
2018-10-27 17:29:20 +00:00
"strings"
2020-05-05 12:34:35 +00:00
"sync"
2020-05-08 15:15:21 +00:00
"time"
2018-10-27 17:29:20 +00:00
2020-08-20 18:01:38 +00:00
"github.com/rclone/rclone/fs/rc/webgui"
2018-10-27 17:29:20 +00:00
"github.com/pkg/errors"
2020-02-26 08:34:32 +00:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/skratchdot/open-golang/open"
2019-07-28 17:47:38 +00:00
"github.com/rclone/rclone/cmd/serve/httplib"
"github.com/rclone/rclone/cmd/serve/httplib/serve"
"github.com/rclone/rclone/fs"
2020-02-26 08:34:32 +00:00
"github.com/rclone/rclone/fs/accounting"
2019-07-28 17:47:38 +00:00
"github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/list"
"github.com/rclone/rclone/fs/rc"
2019-08-08 04:56:58 +00:00
"github.com/rclone/rclone/fs/rc/jobs"
"github.com/rclone/rclone/fs/rc/rcflags"
2020-01-12 09:12:04 +00:00
"github.com/rclone/rclone/lib/random"
2018-10-27 17:29:20 +00:00
)
2020-02-26 08:34:32 +00:00
var promHandler http . Handler
2020-05-05 12:34:35 +00:00
var onlyOnceWarningAllowOrigin sync . Once
2020-02-26 08:34:32 +00:00
func init ( ) {
2020-11-05 16:59:59 +00:00
rcloneCollector := accounting . NewRcloneCollector ( context . Background ( ) )
2020-02-26 08:34:32 +00:00
prometheus . MustRegister ( rcloneCollector )
promHandler = promhttp . Handler ( )
}
2018-10-27 17:29:20 +00:00
// Start the remote control server if configured
2018-11-01 17:20:04 +00:00
//
// If the server wasn't configured the *Server returned may be nil
2020-11-05 15:18:51 +00:00
func Start ( ctx context . Context , opt * rc . Options ) ( * Server , error ) {
2019-08-10 16:12:22 +00:00
jobs . SetOpt ( opt ) // set the defaults for jobs
2018-10-27 17:29:20 +00:00
if opt . Enabled {
2018-10-28 14:31:24 +00:00
// Serve on the DefaultServeMux so can have global registrations appear
2020-11-05 15:18:51 +00:00
s := newServer ( ctx , opt , http . DefaultServeMux )
2018-11-01 17:20:04 +00:00
return s , s . Serve ( )
2018-10-27 17:29:20 +00:00
}
2018-11-01 17:20:04 +00:00
return nil , nil
2018-10-27 17:29:20 +00:00
}
2018-11-01 17:20:04 +00:00
// Server contains everything to run the rc server
type Server struct {
* httplib . Server
2020-11-05 15:18:51 +00:00
ctx context . Context // for global config
2020-07-27 18:32:45 +00:00
files http . Handler
pluginsHandler http . Handler
opt * rc . Options
2018-10-27 17:29:20 +00:00
}
2020-11-05 15:18:51 +00:00
func newServer ( ctx context . Context , opt * rc . Options , mux * http . ServeMux ) * Server {
2020-04-23 19:22:47 +00:00
fileHandler := http . Handler ( nil )
2020-07-27 18:32:45 +00:00
pluginsHandler := http . Handler ( nil )
2018-10-27 17:29:20 +00:00
// Add some more mime types which are often missing
_ = mime . AddExtensionType ( ".wasm" , "application/wasm" )
_ = mime . AddExtensionType ( ".js" , "application/javascript" )
2019-08-04 11:32:37 +00:00
cachePath := filepath . Join ( config . CacheDir , "webgui" )
extractPath := filepath . Join ( cachePath , "current/build" )
2018-10-27 17:29:20 +00:00
// File handling
if opt . Files != "" {
2019-08-04 11:32:37 +00:00
if opt . WebUI {
fs . Logf ( nil , "--rc-files overrides --rc-web-gui command\n" )
}
2018-10-27 17:29:20 +00:00
fs . Logf ( nil , "Serving files from %q" , opt . Files )
2020-04-23 19:22:47 +00:00
fileHandler = http . FileServer ( http . Dir ( opt . Files ) )
2019-08-04 11:32:37 +00:00
} else if opt . WebUI {
2020-05-29 10:05:39 +00:00
if err := webgui . CheckAndDownloadWebGUIRelease ( opt . WebGUIUpdate , opt . WebGUIForceUpdate , opt . WebGUIFetchURL , config . CacheDir ) ; err != nil {
2020-01-12 09:12:04 +00:00
log . Fatalf ( "Error while fetching the latest release of Web GUI: %v" , err )
}
if opt . NoAuth {
opt . NoAuth = false
fs . Infof ( nil , "Cannot run Web GUI without authentication, using default auth" )
}
if opt . HTTPOptions . BasicUser == "" {
opt . HTTPOptions . BasicUser = "gui"
fs . Infof ( nil , "No username specified. Using default username: %s \n" , rcflags . Opt . HTTPOptions . BasicUser )
}
if opt . HTTPOptions . BasicPass == "" {
randomPass , err := random . Password ( 128 )
if err != nil {
log . Fatalf ( "Failed to make password: %v" , err )
}
opt . HTTPOptions . BasicPass = randomPass
fs . Infof ( nil , "No password specified. Using random password: %s \n" , randomPass )
}
opt . Serve = true
fs . Logf ( nil , "Serving Web GUI" )
2020-04-23 19:22:47 +00:00
fileHandler = http . FileServer ( http . Dir ( extractPath ) )
2020-07-27 18:32:45 +00:00
pluginsHandler = http . FileServer ( http . Dir ( webgui . PluginsPath ) )
2018-10-27 17:29:20 +00:00
}
2020-04-23 19:22:47 +00:00
s := & Server {
2020-07-27 18:32:45 +00:00
Server : httplib . NewServer ( mux , & opt . HTTPOptions ) ,
2020-11-05 15:18:51 +00:00
ctx : ctx ,
2020-07-27 18:32:45 +00:00
opt : opt ,
files : fileHandler ,
pluginsHandler : pluginsHandler ,
2020-04-23 19:22:47 +00:00
}
mux . HandleFunc ( "/" , s . handler )
2018-10-27 17:29:20 +00:00
return s
}
2018-11-01 17:20:04 +00:00
// Serve runs the http server in the background.
//
// Use s.Close() and s.Wait() to shutdown server
func ( s * Server ) Serve ( ) error {
err := s . Server . Serve ( )
2018-10-27 17:29:20 +00:00
if err != nil {
2018-11-01 17:20:04 +00:00
return err
2018-10-27 17:29:20 +00:00
}
2018-11-01 17:20:04 +00:00
fs . Logf ( nil , "Serving remote control on %s" , s . URL ( ) )
2018-10-27 17:29:20 +00:00
// Open the files in the browser if set
if s . files != nil {
2018-11-04 11:34:16 +00:00
openURL , err := url . Parse ( s . URL ( ) )
if err != nil {
return errors . Wrap ( err , "invalid serving URL" )
}
// Add username, password into the URL if they are set
user , pass := s . opt . HTTPOptions . BasicUser , s . opt . HTTPOptions . BasicPass
2019-08-20 18:25:04 +00:00
if user != "" && pass != "" {
2018-11-04 11:34:16 +00:00
openURL . User = url . UserPassword ( user , pass )
2019-08-20 18:25:04 +00:00
// Base64 encode username and password to be sent through url
loginToken := user + ":" + pass
parameters := url . Values { }
encodedToken := base64 . URLEncoding . EncodeToString ( [ ] byte ( loginToken ) )
fs . Debugf ( nil , "login_token %q" , encodedToken )
parameters . Add ( "login_token" , encodedToken )
openURL . RawQuery = parameters . Encode ( )
openURL . RawPath = "/#/login"
2018-11-04 11:34:16 +00:00
}
2020-01-12 09:12:04 +00:00
// Don't open browser if serving in testing environment or required not to do so.
if flag . Lookup ( "test.v" ) == nil && ! s . opt . WebGUINoOpenBrowser {
if err := open . Start ( openURL . String ( ) ) ; err != nil {
fs . Errorf ( nil , "Failed to open Web GUI in browser: %v. Manually access it at: %s" , err , openURL . String ( ) )
}
2019-08-06 11:44:08 +00:00
} else {
2020-01-12 09:12:04 +00:00
fs . Logf ( nil , "Web GUI is not automatically opening browser. Navigate to %s to use." , openURL . String ( ) )
2019-07-18 10:13:54 +00:00
}
2018-10-27 17:29:20 +00:00
}
2018-11-01 17:20:04 +00:00
return nil
2018-10-27 17:29:20 +00:00
}
// writeError writes a formatted error to the output
func writeError ( path string , in rc . Params , w http . ResponseWriter , err error , status int ) {
fs . Errorf ( nil , "rc: %q: error: %v" , path , err )
// Adjust the error return for some well known errors
errOrig := errors . Cause ( err )
switch {
case errOrig == fs . ErrorDirNotFound || errOrig == fs . ErrorObjectNotFound :
status = http . StatusNotFound
case rc . IsErrParamInvalid ( err ) || rc . IsErrParamNotFound ( err ) :
status = http . StatusBadRequest
}
w . WriteHeader ( status )
err = rc . WriteJSON ( w , rc . Params {
"status" : status ,
"error" : err . Error ( ) ,
"input" : in ,
"path" : path ,
} )
if err != nil {
// can't return the error at this point
2021-01-10 12:24:22 +00:00
fs . Errorf ( nil , "rc: writeError: failed to write JSON output from %#v: %v" , in , err )
2018-10-27 17:29:20 +00:00
}
}
// handler reads incoming requests and dispatches them
2018-11-01 17:20:04 +00:00
func ( s * Server ) handler ( w http . ResponseWriter , r * http . Request ) {
2019-08-20 18:47:57 +00:00
urlPath , ok := s . Path ( w , r )
if ! ok {
return
}
path := strings . TrimLeft ( urlPath , "/" )
2018-10-27 17:29:20 +00:00
2019-08-08 04:56:58 +00:00
allowOrigin := rcflags . Opt . AccessControlAllowOrigin
if allowOrigin != "" {
2020-05-05 12:34:35 +00:00
onlyOnceWarningAllowOrigin . Do ( func ( ) {
if allowOrigin == "*" {
fs . Logf ( nil , "Warning: Allow origin set to *. This can cause serious security problems." )
}
} )
2019-08-08 04:56:58 +00:00
w . Header ( ) . Add ( "Access-Control-Allow-Origin" , allowOrigin )
} else {
w . Header ( ) . Add ( "Access-Control-Allow-Origin" , s . URL ( ) )
}
2018-10-27 17:29:20 +00:00
// echo back access control headers client needs
2019-08-04 11:32:37 +00:00
//reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers")
w . Header ( ) . Add ( "Access-Control-Request-Method" , "POST, OPTIONS, GET, HEAD" )
w . Header ( ) . Add ( "Access-Control-Allow-Headers" , "authorization, Content-Type" )
2018-10-27 17:29:20 +00:00
switch r . Method {
case "POST" :
s . handlePost ( w , r , path )
case "OPTIONS" :
s . handleOptions ( w , r , path )
2018-10-28 14:31:24 +00:00
case "GET" , "HEAD" :
2018-10-27 17:29:20 +00:00
s . handleGet ( w , r , path )
default :
writeError ( path , nil , w , errors . Errorf ( "method %q not allowed" , r . Method ) , http . StatusMethodNotAllowed )
return
}
}
2018-11-01 17:20:04 +00:00
func ( s * Server ) handlePost ( w http . ResponseWriter , r * http . Request , path string ) {
2020-12-12 15:35:30 +00:00
ctx := r . Context ( )
2018-10-28 14:31:24 +00:00
contentType := r . Header . Get ( "Content-Type" )
values := r . URL . Query ( )
if contentType == "application/x-www-form-urlencoded" {
// Parse the POST and URL parameters into r.Form, for others r.Form will be empty value
err := r . ParseForm ( )
if err != nil {
writeError ( path , nil , w , errors . Wrap ( err , "failed to parse form/URL parameters" ) , http . StatusBadRequest )
return
}
values = r . Form
2018-10-27 17:29:20 +00:00
}
// Read the POST and URL parameters into in
in := make ( rc . Params )
2018-10-28 14:31:24 +00:00
for k , vs := range values {
2018-10-27 17:29:20 +00:00
if len ( vs ) > 0 {
in [ k ] = vs [ len ( vs ) - 1 ]
}
}
// Parse a JSON blob from the input
2018-10-28 14:31:24 +00:00
if contentType == "application/json" {
2018-10-27 17:29:20 +00:00
err := json . NewDecoder ( r . Body ) . Decode ( & in )
if err != nil {
writeError ( path , in , w , errors . Wrap ( err , "failed to read input JSON" ) , http . StatusBadRequest )
return
}
}
// Find the call
call := rc . Calls . Get ( path )
if call == nil {
2018-10-28 14:31:24 +00:00
writeError ( path , in , w , errors . Errorf ( "couldn't find method %q" , path ) , http . StatusNotFound )
2018-10-27 17:29:20 +00:00
return
}
2018-11-03 16:37:09 +00:00
// Check to see if it requires authorisation
if ! s . opt . NoAuth && call . AuthRequired && ! s . UsingAuth ( ) {
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 )
return
}
2021-01-10 12:24:22 +00:00
inOrig := in . Copy ( )
2020-06-05 14:26:46 +00:00
if call . NeedsRequest {
// Add the request to RC
in [ "_request" ] = r
}
2018-11-03 16:37:09 +00:00
2020-07-27 18:01:35 +00:00
if call . NeedsResponse {
2020-08-15 18:03:07 +00:00
in [ "_response" ] = w
2020-07-27 18:01:35 +00:00
}
2018-10-27 17:29:20 +00:00
fs . Debugf ( nil , "rc: %q: with parameters %+v" , path , in )
2020-12-12 15:35:30 +00:00
job , out , err := jobs . NewJob ( ctx , call . Fn , in )
if job != nil {
w . Header ( ) . Add ( "x-rclone-jobid" , fmt . Sprintf ( "%d" , job . ID ) )
2018-10-27 17:29:20 +00:00
}
if err != nil {
2021-01-10 12:24:22 +00:00
writeError ( path , inOrig , w , err , http . StatusInternalServerError )
2018-10-27 17:29:20 +00:00
return
}
if out == nil {
out = make ( rc . Params )
}
fs . Debugf ( nil , "rc: %q: reply %+v: %v" , path , out , err )
err = rc . WriteJSON ( w , out )
if err != nil {
2019-08-10 15:22:17 +00:00
// can't return the error at this point - but have a go anyway
2021-01-10 12:24:22 +00:00
writeError ( path , inOrig , w , err , http . StatusInternalServerError )
fs . Errorf ( nil , "rc: handlePost: failed to write JSON output: %v" , err )
2018-10-27 17:29:20 +00:00
}
}
2018-11-01 17:20:04 +00:00
func ( s * Server ) handleOptions ( w http . ResponseWriter , r * http . Request , path string ) {
2018-10-27 17:29:20 +00:00
w . WriteHeader ( http . StatusOK )
}
2018-11-01 17:20:04 +00:00
func ( s * Server ) serveRoot ( w http . ResponseWriter , r * http . Request ) {
2018-10-28 14:31:24 +00:00
remotes := config . FileSections ( )
sort . Strings ( remotes )
2018-12-23 00:16:50 +00:00
directory := serve . NewDirectory ( "" , s . HTMLTemplate )
2020-05-08 15:15:21 +00:00
directory . Name = "List of all rclone remotes."
2018-10-28 14:31:24 +00:00
q := url . Values { }
for _ , remote := range remotes {
q . Set ( "fs" , remote )
2020-05-08 15:15:21 +00:00
directory . AddHTMLEntry ( "[" + remote + ":]" , true , - 1 , time . Time { } )
2018-10-28 14:31:24 +00:00
}
2020-05-08 15:15:21 +00:00
sortParm := r . URL . Query ( ) . Get ( "sort" )
orderParm := r . URL . Query ( ) . Get ( "order" )
directory . ProcessQueryParams ( sortParm , orderParm )
2018-10-28 14:31:24 +00:00
directory . Serve ( w , r )
}
2018-11-01 17:20:04 +00:00
func ( s * Server ) serveRemote ( w http . ResponseWriter , r * http . Request , path string , fsName string ) {
2020-11-05 15:18:51 +00:00
f , err := cache . Get ( s . ctx , fsName )
2018-10-28 14:31:24 +00:00
if err != nil {
writeError ( path , nil , w , errors . Wrap ( err , "failed to make Fs" ) , http . StatusInternalServerError )
return
}
if path == "" || strings . HasSuffix ( path , "/" ) {
path = strings . Trim ( path , "/" )
2019-06-17 08:34:30 +00:00
entries , err := list . DirSorted ( r . Context ( ) , f , false , path )
2018-10-27 17:29:20 +00:00
if err != nil {
2018-10-28 14:31:24 +00:00
writeError ( path , nil , w , errors . Wrap ( err , "failed to list directory" ) , http . StatusInternalServerError )
2018-10-27 17:29:20 +00:00
return
}
2018-10-28 14:31:24 +00:00
// Make the entries for display
2018-12-23 00:16:50 +00:00
directory := serve . NewDirectory ( path , s . HTMLTemplate )
2018-10-28 14:31:24 +00:00
for _ , entry := range entries {
_ , isDir := entry . ( fs . Directory )
2020-05-08 15:15:21 +00:00
//directory.AddHTMLEntry(entry.Remote(), isDir, entry.Size(), entry.ModTime(r.Context()))
directory . AddHTMLEntry ( entry . Remote ( ) , isDir , entry . Size ( ) , time . Time { } )
2018-10-28 14:31:24 +00:00
}
2020-05-08 15:15:21 +00:00
sortParm := r . URL . Query ( ) . Get ( "sort" )
orderParm := r . URL . Query ( ) . Get ( "order" )
directory . ProcessQueryParams ( sortParm , orderParm )
2018-10-28 14:31:24 +00:00
directory . Serve ( w , r )
} else {
2019-06-10 10:59:06 +00:00
path = strings . Trim ( path , "/" )
2019-06-17 08:34:30 +00:00
o , err := f . NewObject ( r . Context ( ) , path )
2018-10-27 17:29:20 +00:00
if err != nil {
writeError ( path , nil , w , errors . Wrap ( err , "failed to find object" ) , http . StatusInternalServerError )
return
}
serve . Object ( w , r , o )
2018-10-28 14:31:24 +00:00
}
}
// Match URLS of the form [fs]/remote
var fsMatch = regexp . MustCompile ( ` ^\[(.*?)\](.*)$ ` )
2018-11-01 17:20:04 +00:00
func ( s * Server ) handleGet ( w http . ResponseWriter , r * http . Request , path string ) {
2018-10-28 14:31:24 +00:00
// Look to see if this has an fs in the path
2020-07-27 18:32:45 +00:00
fsMatchResult := fsMatch . FindStringSubmatch ( path )
2018-10-28 14:31:24 +00:00
switch {
2020-07-27 18:32:45 +00:00
case fsMatchResult != nil && s . opt . Serve :
2018-10-28 14:31:24 +00:00
// Serve /[fs]/remote files
2020-07-27 18:32:45 +00:00
s . serveRemote ( w , r , fsMatchResult [ 2 ] , fsMatchResult [ 1 ] )
2018-10-28 14:31:24 +00:00
return
2020-02-26 08:34:32 +00:00
case path == "metrics" && s . opt . EnableMetrics :
promHandler . ServeHTTP ( w , r )
return
2018-10-28 14:31:24 +00:00
case path == "*" && s . opt . Serve :
// Serve /* as the remote listing
s . serveRoot ( w , r )
return
case s . files != nil :
2020-09-17 19:06:51 +00:00
if s . opt . WebUI {
pluginsMatchResult := webgui . PluginsMatch . FindStringSubmatch ( path )
if pluginsMatchResult != nil && len ( pluginsMatchResult ) > 2 {
ok := webgui . ServePluginOK ( w , r , pluginsMatchResult )
if ! ok {
r . URL . Path = fmt . Sprintf ( "/%s/%s/app/build/%s" , pluginsMatchResult [ 1 ] , pluginsMatchResult [ 2 ] , pluginsMatchResult [ 3 ] )
s . pluginsHandler . ServeHTTP ( w , r )
return
}
return
} else if webgui . ServePluginWithReferrerOK ( w , r , path ) {
2020-07-27 18:32:45 +00:00
return
}
}
2018-10-28 14:31:24 +00:00
// Serve the files
2019-08-20 18:47:57 +00:00
r . URL . Path = "/" + path
2018-10-27 17:29:20 +00:00
s . files . ServeHTTP ( w , r )
2018-10-28 14:31:24 +00:00
return
case path == "" && s . opt . Serve :
// Serve the root as a remote listing
s . serveRoot ( w , r )
return
2018-10-27 17:29:20 +00:00
}
2018-10-28 14:31:24 +00:00
http . Error ( w , http . StatusText ( http . StatusNotFound ) , http . StatusNotFound )
2018-10-27 17:29:20 +00:00
}