2018-10-28 01:29:20 +08:00
// Package rcserver implements the HTTP endpoint to serve the remote control
package rcserver
import (
2019-08-21 02:25:04 +08:00
"encoding/base64"
2018-10-28 01:29:20 +08:00
"encoding/json"
2019-07-18 18:13:54 +08:00
"flag"
"fmt"
2020-01-12 17:12:04 +08:00
"log"
2018-10-28 01:29:20 +08:00
"mime"
"net/http"
2018-10-28 22:31:24 +08:00
"net/url"
2019-08-04 19:32:37 +08:00
"path/filepath"
2018-10-28 22:31:24 +08:00
"regexp"
"sort"
2018-10-28 01:29:20 +08:00
"strings"
"github.com/pkg/errors"
2020-02-26 16:34:32 +08:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/skratchdot/open-golang/open"
2019-07-29 01:47:38 +08:00
"github.com/rclone/rclone/cmd/serve/httplib"
"github.com/rclone/rclone/cmd/serve/httplib/serve"
"github.com/rclone/rclone/fs"
2020-02-26 16:34:32 +08:00
"github.com/rclone/rclone/fs/accounting"
2019-07-29 01:47:38 +08: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 12:56:58 +08:00
"github.com/rclone/rclone/fs/rc/jobs"
"github.com/rclone/rclone/fs/rc/rcflags"
2020-01-12 17:12:04 +08:00
"github.com/rclone/rclone/lib/random"
2018-10-28 01:29:20 +08:00
)
2020-02-26 16:34:32 +08:00
var promHandler http . Handler
func init ( ) {
rcloneCollector := accounting . NewRcloneCollector ( )
prometheus . MustRegister ( rcloneCollector )
promHandler = promhttp . Handler ( )
}
2018-10-28 01:29:20 +08:00
// Start the remote control server if configured
2018-11-02 01:20:04 +08:00
//
// If the server wasn't configured the *Server returned may be nil
func Start ( opt * rc . Options ) ( * Server , error ) {
2019-08-11 00:12:22 +08:00
jobs . SetOpt ( opt ) // set the defaults for jobs
2018-10-28 01:29:20 +08:00
if opt . Enabled {
2018-10-28 22:31:24 +08:00
// Serve on the DefaultServeMux so can have global registrations appear
s := newServer ( opt , http . DefaultServeMux )
2018-11-02 01:20:04 +08:00
return s , s . Serve ( )
2018-10-28 01:29:20 +08:00
}
2018-11-02 01:20:04 +08:00
return nil , nil
2018-10-28 01:29:20 +08:00
}
2018-11-02 01:20:04 +08:00
// Server contains everything to run the rc server
type Server struct {
* httplib . Server
2018-10-28 01:29:20 +08:00
files http . Handler
2018-10-28 22:31:24 +08:00
opt * rc . Options
2018-10-28 01:29:20 +08:00
}
2018-11-02 01:20:04 +08:00
func newServer ( opt * rc . Options , mux * http . ServeMux ) * Server {
s := & Server {
Server : httplib . NewServer ( mux , & opt . HTTPOptions ) ,
opt : opt ,
2018-10-28 01:29:20 +08:00
}
mux . HandleFunc ( "/" , s . handler )
// Add some more mime types which are often missing
_ = mime . AddExtensionType ( ".wasm" , "application/wasm" )
_ = mime . AddExtensionType ( ".js" , "application/javascript" )
2019-08-04 19:32:37 +08:00
cachePath := filepath . Join ( config . CacheDir , "webgui" )
extractPath := filepath . Join ( cachePath , "current/build" )
2018-10-28 01:29:20 +08:00
// File handling
if opt . Files != "" {
2019-08-04 19:32:37 +08:00
if opt . WebUI {
fs . Logf ( nil , "--rc-files overrides --rc-web-gui command\n" )
}
2018-10-28 01:29:20 +08:00
fs . Logf ( nil , "Serving files from %q" , opt . Files )
s . files = http . FileServer ( http . Dir ( opt . Files ) )
2019-08-04 19:32:37 +08:00
} else if opt . WebUI {
2020-01-12 17:12:04 +08:00
if err := rc . CheckAndDownloadWebGUIRelease ( opt . WebGUIUpdate , opt . WebGUIForceUpdate , opt . WebGUIFetchURL , config . CacheDir ) ; err != nil {
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" )
2019-08-04 19:32:37 +08:00
s . files = http . FileServer ( http . Dir ( extractPath ) )
2018-10-28 01:29:20 +08:00
}
return s
}
2018-11-02 01:20:04 +08: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-28 01:29:20 +08:00
if err != nil {
2018-11-02 01:20:04 +08:00
return err
2018-10-28 01:29:20 +08:00
}
2018-11-02 01:20:04 +08:00
fs . Logf ( nil , "Serving remote control on %s" , s . URL ( ) )
2018-10-28 01:29:20 +08:00
// Open the files in the browser if set
if s . files != nil {
2018-11-04 19:34:16 +08: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-21 02:25:04 +08:00
if user != "" && pass != "" {
2018-11-04 19:34:16 +08:00
openURL . User = url . UserPassword ( user , pass )
2019-08-21 02:25:04 +08: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 19:34:16 +08:00
}
2020-01-12 17:12:04 +08: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 19:44:08 +08:00
} else {
2020-01-12 17:12:04 +08:00
fs . Logf ( nil , "Web GUI is not automatically opening browser. Navigate to %s to use." , openURL . String ( ) )
2019-07-18 18:13:54 +08:00
}
2018-10-28 01:29:20 +08:00
}
2018-11-02 01:20:04 +08:00
return nil
2018-10-28 01:29:20 +08: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
fs . Errorf ( nil , "rc: failed to write JSON output: %v" , err )
}
}
// handler reads incoming requests and dispatches them
2018-11-02 01:20:04 +08:00
func ( s * Server ) handler ( w http . ResponseWriter , r * http . Request ) {
2019-08-21 02:47:57 +08:00
urlPath , ok := s . Path ( w , r )
if ! ok {
return
}
path := strings . TrimLeft ( urlPath , "/" )
2018-10-28 01:29:20 +08:00
2019-08-08 12:56:58 +08:00
allowOrigin := rcflags . Opt . AccessControlAllowOrigin
if allowOrigin != "" {
if allowOrigin == "*" {
fs . Logf ( nil , "Warning: Allow origin set to *. This can cause serious security problems." )
}
w . Header ( ) . Add ( "Access-Control-Allow-Origin" , allowOrigin )
} else {
w . Header ( ) . Add ( "Access-Control-Allow-Origin" , s . URL ( ) )
}
2018-10-28 01:29:20 +08:00
// echo back access control headers client needs
2019-08-04 19:32:37 +08: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-28 01:29:20 +08:00
switch r . Method {
case "POST" :
s . handlePost ( w , r , path )
case "OPTIONS" :
s . handleOptions ( w , r , path )
2018-10-28 22:31:24 +08:00
case "GET" , "HEAD" :
2018-10-28 01:29:20 +08:00
s . handleGet ( w , r , path )
default :
writeError ( path , nil , w , errors . Errorf ( "method %q not allowed" , r . Method ) , http . StatusMethodNotAllowed )
return
}
}
2018-11-02 01:20:04 +08:00
func ( s * Server ) handlePost ( w http . ResponseWriter , r * http . Request , path string ) {
2018-10-28 22:31:24 +08: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-28 01:29:20 +08:00
}
// Read the POST and URL parameters into in
in := make ( rc . Params )
2018-10-28 22:31:24 +08:00
for k , vs := range values {
2018-10-28 01:29:20 +08:00
if len ( vs ) > 0 {
in [ k ] = vs [ len ( vs ) - 1 ]
}
}
// Parse a JSON blob from the input
2018-10-28 22:31:24 +08:00
if contentType == "application/json" {
2018-10-28 01:29:20 +08: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 22:31:24 +08:00
writeError ( path , in , w , errors . Errorf ( "couldn't find method %q" , path ) , http . StatusNotFound )
2018-10-28 01:29:20 +08:00
return
}
2018-11-04 00:37:09 +08: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
}
2018-10-28 01:29:20 +08:00
// Check to see if it is async or not
isAsync , err := in . GetBool ( "_async" )
if rc . NotErrParamNotFound ( err ) {
writeError ( path , in , w , err , http . StatusBadRequest )
return
}
2019-07-21 00:12:40 +08:00
delete ( in , "_async" ) // remove the async parameter after parsing so vfs operations don't get confused
2018-10-28 01:29:20 +08:00
fs . Debugf ( nil , "rc: %q: with parameters %+v" , path , in )
var out rc . Params
if isAsync {
2019-07-18 18:13:54 +08:00
out , err = jobs . StartAsyncJob ( call . Fn , in )
2018-10-28 01:29:20 +08:00
} else {
2019-07-18 18:13:54 +08:00
var jobID int64
out , jobID , err = jobs . ExecuteJob ( r . Context ( ) , call . Fn , in )
w . Header ( ) . Add ( "x-rclone-jobid" , fmt . Sprintf ( "%d" , jobID ) )
2018-10-28 01:29:20 +08:00
}
if err != nil {
writeError ( path , in , w , err , http . StatusInternalServerError )
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 23:22:17 +08:00
// can't return the error at this point - but have a go anyway
writeError ( path , in , w , err , http . StatusInternalServerError )
2018-10-28 01:29:20 +08:00
fs . Errorf ( nil , "rc: failed to write JSON output: %v" , err )
}
}
2018-11-02 01:20:04 +08:00
func ( s * Server ) handleOptions ( w http . ResponseWriter , r * http . Request , path string ) {
2018-10-28 01:29:20 +08:00
w . WriteHeader ( http . StatusOK )
}
2018-11-02 01:20:04 +08:00
func ( s * Server ) serveRoot ( w http . ResponseWriter , r * http . Request ) {
2018-10-28 22:31:24 +08:00
remotes := config . FileSections ( )
sort . Strings ( remotes )
2018-12-23 08:16:50 +08:00
directory := serve . NewDirectory ( "" , s . HTMLTemplate )
2018-10-28 22:31:24 +08:00
directory . Title = "List of all rclone remotes."
q := url . Values { }
for _ , remote := range remotes {
q . Set ( "fs" , remote )
directory . AddEntry ( "[" + remote + ":]" , true )
}
directory . Serve ( w , r )
}
2018-11-02 01:20:04 +08:00
func ( s * Server ) serveRemote ( w http . ResponseWriter , r * http . Request , path string , fsName string ) {
2019-05-23 19:26:16 +08:00
f , err := cache . Get ( fsName )
2018-10-28 22:31:24 +08: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 16:34:30 +08:00
entries , err := list . DirSorted ( r . Context ( ) , f , false , path )
2018-10-28 01:29:20 +08:00
if err != nil {
2018-10-28 22:31:24 +08:00
writeError ( path , nil , w , errors . Wrap ( err , "failed to list directory" ) , http . StatusInternalServerError )
2018-10-28 01:29:20 +08:00
return
}
2018-10-28 22:31:24 +08:00
// Make the entries for display
2018-12-23 08:16:50 +08:00
directory := serve . NewDirectory ( path , s . HTMLTemplate )
2018-10-28 22:31:24 +08:00
for _ , entry := range entries {
_ , isDir := entry . ( fs . Directory )
directory . AddEntry ( entry . Remote ( ) , isDir )
}
directory . Serve ( w , r )
} else {
2019-06-10 18:59:06 +08:00
path = strings . Trim ( path , "/" )
2019-06-17 16:34:30 +08:00
o , err := f . NewObject ( r . Context ( ) , path )
2018-10-28 01:29:20 +08: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 22:31:24 +08:00
}
}
// Match URLS of the form [fs]/remote
var fsMatch = regexp . MustCompile ( ` ^\[(.*?)\](.*)$ ` )
2018-11-02 01:20:04 +08:00
func ( s * Server ) handleGet ( w http . ResponseWriter , r * http . Request , path string ) {
2018-10-28 22:31:24 +08:00
// Look to see if this has an fs in the path
match := fsMatch . FindStringSubmatch ( path )
switch {
case match != nil && s . opt . Serve :
// Serve /[fs]/remote files
s . serveRemote ( w , r , match [ 2 ] , match [ 1 ] )
return
2020-02-26 16:34:32 +08:00
case path == "metrics" && s . opt . EnableMetrics :
promHandler . ServeHTTP ( w , r )
return
2018-10-28 22:31:24 +08:00
case path == "*" && s . opt . Serve :
// Serve /* as the remote listing
s . serveRoot ( w , r )
return
case s . files != nil :
// Serve the files
2019-08-21 02:47:57 +08:00
r . URL . Path = "/" + path
2018-10-28 01:29:20 +08:00
s . files . ServeHTTP ( w , r )
2018-10-28 22:31:24 +08:00
return
case path == "" && s . opt . Serve :
// Serve the root as a remote listing
s . serveRoot ( w , r )
return
2018-10-28 01:29:20 +08:00
}
2018-10-28 22:31:24 +08:00
http . Error ( w , http . StatusText ( http . StatusNotFound ) , http . StatusNotFound )
2018-10-28 01:29:20 +08:00
}