mirror of
https://github.com/rclone/rclone.git
synced 2025-12-06 00:03:32 +00:00
rcd: refactor rclone rc server to use lib/http
This commit is contained in:
@@ -18,9 +18,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rclone/rclone/cmd/serve/httplib"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/accounting"
|
||||
"github.com/rclone/rclone/fs/cache"
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/rclone/rclone/fs/rc/jobs"
|
||||
"github.com/rclone/rclone/fs/rc/rcflags"
|
||||
"github.com/rclone/rclone/fs/rc/webgui"
|
||||
libhttp "github.com/rclone/rclone/lib/http"
|
||||
"github.com/rclone/rclone/lib/http/serve"
|
||||
"github.com/rclone/rclone/lib/random"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
@@ -59,7 +60,10 @@ func Start(ctx context.Context, opt *rc.Options) (*Server, error) {
|
||||
jobs.SetOpt(opt) // set the defaults for jobs
|
||||
if opt.Enabled {
|
||||
// Serve on the DefaultServeMux so can have global registrations appear
|
||||
s := newServer(ctx, opt, http.DefaultServeMux)
|
||||
s, err := newServer(ctx, opt, http.DefaultServeMux)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, s.Serve()
|
||||
}
|
||||
return nil, nil
|
||||
@@ -67,14 +71,14 @@ func Start(ctx context.Context, opt *rc.Options) (*Server, error) {
|
||||
|
||||
// Server contains everything to run the rc server
|
||||
type Server struct {
|
||||
*httplib.Server
|
||||
ctx context.Context // for global config
|
||||
server *libhttp.Server
|
||||
files http.Handler
|
||||
pluginsHandler http.Handler
|
||||
opt *rc.Options
|
||||
}
|
||||
|
||||
func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) *Server {
|
||||
func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) (*Server, error) {
|
||||
fileHandler := http.Handler(nil)
|
||||
pluginsHandler := http.Handler(nil)
|
||||
// Add some more mime types which are often missing
|
||||
@@ -97,16 +101,16 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) *Server
|
||||
if opt.NoAuth {
|
||||
fs.Logf(nil, "It is recommended to use web gui with auth.")
|
||||
} else {
|
||||
if opt.HTTPOptions.BasicUser == "" && opt.HTTPOptions.HtPasswd == "" {
|
||||
opt.HTTPOptions.BasicUser = "gui"
|
||||
fs.Infof(nil, "No username specified. Using default username: %s \n", rcflags.Opt.HTTPOptions.BasicUser)
|
||||
if opt.Auth.BasicUser == "" && opt.Auth.HtPasswd == "" {
|
||||
opt.Auth.BasicUser = "gui"
|
||||
fs.Infof(nil, "No username specified. Using default username: %s \n", rcflags.Opt.Auth.BasicUser)
|
||||
}
|
||||
if opt.HTTPOptions.BasicPass == "" && opt.HTTPOptions.HtPasswd == "" {
|
||||
if opt.Auth.BasicPass == "" && opt.Auth.HtPasswd == "" {
|
||||
randomPass, err := random.Password(128)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to make password: %v", err)
|
||||
}
|
||||
opt.HTTPOptions.BasicPass = randomPass
|
||||
opt.Auth.BasicPass = randomPass
|
||||
fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
|
||||
}
|
||||
}
|
||||
@@ -119,53 +123,76 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) *Server
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Server: httplib.NewServer(mux, &opt.HTTPOptions),
|
||||
ctx: ctx,
|
||||
opt: opt,
|
||||
files: fileHandler,
|
||||
pluginsHandler: pluginsHandler,
|
||||
}
|
||||
mux.HandleFunc("/", s.handler)
|
||||
|
||||
return s
|
||||
var err error
|
||||
s.server, err = libhttp.NewServer(ctx,
|
||||
libhttp.WithConfig(opt.HTTP),
|
||||
libhttp.WithAuth(opt.Auth),
|
||||
libhttp.WithTemplate(opt.Template),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init server: %w", err)
|
||||
}
|
||||
|
||||
router := s.server.Router()
|
||||
router.Use(
|
||||
middleware.SetHeader("Accept-Ranges", "bytes"),
|
||||
middleware.SetHeader("Server", "rclone/"+fs.Version),
|
||||
)
|
||||
|
||||
// Add the debug handler which is installed in the default mux
|
||||
router.Handle("/debug/*", mux)
|
||||
|
||||
// FIXME split these up into individual functions
|
||||
router.Get("/*", s.handler)
|
||||
router.Head("/*", s.handler)
|
||||
router.Post("/*", s.handler)
|
||||
router.Options("/*", s.handler)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fs.Logf(nil, "Serving remote control on %s", s.URL())
|
||||
// Open the files in the browser if set
|
||||
if s.files != nil {
|
||||
openURL, err := url.Parse(s.URL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid serving URL: %w", err)
|
||||
}
|
||||
// Add username, password into the URL if they are set
|
||||
user, pass := s.opt.HTTPOptions.BasicUser, s.opt.HTTPOptions.BasicPass
|
||||
if user != "" && pass != "" {
|
||||
openURL.User = url.UserPassword(user, pass)
|
||||
s.server.Serve()
|
||||
|
||||
// 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"
|
||||
}
|
||||
// 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())
|
||||
for _, URL := range s.server.URLs() {
|
||||
fs.Logf(nil, "Serving remote control on %s", URL)
|
||||
// Open the files in the browser if set
|
||||
if s.files != nil {
|
||||
openURL, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid serving URL: %w", err)
|
||||
}
|
||||
// Add username, password into the URL if they are set
|
||||
user, pass := s.opt.Auth.BasicUser, s.opt.Auth.BasicPass
|
||||
if user != "" && pass != "" {
|
||||
openURL.User = url.UserPassword(user, pass)
|
||||
|
||||
// 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"
|
||||
}
|
||||
// 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())
|
||||
}
|
||||
} else {
|
||||
fs.Logf(nil, "Web GUI is not automatically opening browser. Navigate to %s to use.", openURL.String())
|
||||
}
|
||||
} else {
|
||||
fs.Logf(nil, "Web GUI is not automatically opening browser. Navigate to %s to use.", openURL.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -185,11 +212,7 @@ func writeError(path string, in rc.Params, w http.ResponseWriter, err error, sta
|
||||
|
||||
// handler reads incoming requests and dispatches them
|
||||
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
urlPath, ok := s.Path(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
path := strings.TrimLeft(urlPath, "/")
|
||||
path := strings.TrimLeft(r.URL.Path, "/")
|
||||
|
||||
allowOrigin := rcflags.Opt.AccessControlAllowOrigin
|
||||
if allowOrigin != "" {
|
||||
@@ -200,7 +223,12 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
w.Header().Add("Access-Control-Allow-Origin", allowOrigin)
|
||||
} else {
|
||||
w.Header().Add("Access-Control-Allow-Origin", s.URL())
|
||||
urls := s.server.URLs()
|
||||
if len(urls) == 1 {
|
||||
w.Header().Add("Access-Control-Allow-Origin", urls[0])
|
||||
} else {
|
||||
fs.Errorf(nil, "Warning, need exactly 1 URL for Access-Control-Allow-Origin, got %d %q", len(urls), urls)
|
||||
}
|
||||
}
|
||||
|
||||
// echo back access control headers client needs
|
||||
@@ -260,7 +288,7 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string)
|
||||
}
|
||||
|
||||
// Check to see if it requires authorisation
|
||||
if !s.opt.NoAuth && call.AuthRequired && !s.UsingAuth() {
|
||||
if !s.opt.NoAuth && call.AuthRequired && !s.server.UsingAuth() {
|
||||
writeError(path, in, w, fmt.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
|
||||
}
|
||||
@@ -305,7 +333,7 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request, path stri
|
||||
func (s *Server) serveRoot(w http.ResponseWriter, r *http.Request) {
|
||||
remotes := config.FileSections()
|
||||
sort.Strings(remotes)
|
||||
directory := serve.NewDirectory("", s.HTMLTemplate)
|
||||
directory := serve.NewDirectory("", s.server.HTMLTemplate())
|
||||
directory.Name = "List of all rclone remotes."
|
||||
q := url.Values{}
|
||||
for _, remote := range remotes {
|
||||
@@ -333,7 +361,7 @@ func (s *Server) serveRemote(w http.ResponseWriter, r *http.Request, path string
|
||||
return
|
||||
}
|
||||
// Make the entries for display
|
||||
directory := serve.NewDirectory(path, s.HTMLTemplate)
|
||||
directory := serve.NewDirectory(path, s.server.HTMLTemplate())
|
||||
for _, entry := range entries {
|
||||
_, isDir := entry.(fs.Directory)
|
||||
//directory.AddHTMLEntry(entry.Remote(), isDir, entry.Size(), entry.ModTime(r.Context()))
|
||||
@@ -401,3 +429,13 @@ func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, path string)
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// Wait blocks while the server is serving requests
|
||||
func (s *Server) Wait() {
|
||||
s.server.Wait()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server
|
||||
func (s *Server) Shutdown() error {
|
||||
return s.server.Shutdown()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user