mirror of
https://github.com/rclone/rclone.git
synced 2024-11-23 00:43:49 +01:00
300 lines
7.9 KiB
Go
300 lines
7.9 KiB
Go
// Package webgui provides plugin functionality to the Web GUI.
|
|
package webgui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/fs/rc/rcflags"
|
|
)
|
|
|
|
// PackageJSON is the structure of package.json of a plugin
|
|
type PackageJSON struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Description string `json:"description"`
|
|
Author string `json:"author"`
|
|
Copyright string `json:"copyright"`
|
|
License string `json:"license"`
|
|
Private bool `json:"private"`
|
|
Homepage string `json:"homepage"`
|
|
TestURL string `json:"testUrl"`
|
|
Repository struct {
|
|
Type string `json:"type"`
|
|
URL string `json:"url"`
|
|
} `json:"repository"`
|
|
Bugs struct {
|
|
URL string `json:"url"`
|
|
} `json:"bugs"`
|
|
Rclone RcloneConfig `json:"rclone"`
|
|
}
|
|
|
|
// RcloneConfig represents the rclone specific config
|
|
type RcloneConfig struct {
|
|
HandlesType []string `json:"handlesType"`
|
|
PluginType string `json:"pluginType"`
|
|
RedirectReferrer bool `json:"redirectReferrer"`
|
|
Test bool `json:"-"`
|
|
}
|
|
|
|
func (r *PackageJSON) isTesting() bool {
|
|
return r.Rclone.Test
|
|
}
|
|
|
|
var (
|
|
//loadedTestPlugins *Plugins
|
|
cachePath string
|
|
|
|
loadedPlugins *Plugins
|
|
pluginsProxy = &httputil.ReverseProxy{}
|
|
// PluginsMatch is used for matching author and plugin name in the url path
|
|
PluginsMatch = regexp.MustCompile(`^plugins\/([^\/]*)\/([^\/\?]+)[\/]?(.*)$`)
|
|
// PluginsPath is the base path where webgui plugins are stored
|
|
PluginsPath string
|
|
pluginsConfigPath string
|
|
availablePluginsJSONPath = "availablePlugins.json"
|
|
initSuccess = false
|
|
initMutex = &sync.Mutex{}
|
|
)
|
|
|
|
// Plugins represents the structure how plugins are saved onto disk
|
|
type Plugins struct {
|
|
mutex sync.Mutex
|
|
LoadedPlugins map[string]PackageJSON `json:"loadedPlugins"`
|
|
fileName string
|
|
}
|
|
|
|
func newPlugins(fileName string) *Plugins {
|
|
p := Plugins{LoadedPlugins: map[string]PackageJSON{}}
|
|
p.fileName = fileName
|
|
p.mutex = sync.Mutex{}
|
|
return &p
|
|
}
|
|
|
|
func initPluginsOrError() error {
|
|
if !rcflags.Opt.WebUI {
|
|
return errors.New("WebUI needs to be enabled for plugins to work")
|
|
}
|
|
initMutex.Lock()
|
|
defer initMutex.Unlock()
|
|
if !initSuccess {
|
|
cachePath = filepath.Join(config.GetCacheDir(), "webgui")
|
|
PluginsPath = filepath.Join(cachePath, "plugins")
|
|
pluginsConfigPath = filepath.Join(PluginsPath, "config")
|
|
loadedPlugins = newPlugins(availablePluginsJSONPath)
|
|
err := loadedPlugins.readFromFile()
|
|
if err != nil {
|
|
fs.Errorf(nil, "error reading available plugins: %v", err)
|
|
}
|
|
initSuccess = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugins) readFromFile() (err error) {
|
|
err = CreatePathIfNotExist(pluginsConfigPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)
|
|
_, err = os.Stat(availablePluginsJSON)
|
|
if err == nil {
|
|
data, err := os.ReadFile(availablePluginsJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(data, &p)
|
|
if err != nil {
|
|
fs.Logf(nil, "%s", err)
|
|
}
|
|
return nil
|
|
} else if os.IsNotExist(err) {
|
|
// path does not exist
|
|
err = p.writeToFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugins) addPlugin(pluginName string, packageJSONPath string) (err error) {
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
data, err := os.ReadFile(packageJSONPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var pkgJSON = PackageJSON{}
|
|
err = json.Unmarshal(data, &pkgJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.LoadedPlugins[pluginName] = pkgJSON
|
|
|
|
err = p.writeToFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugins) writeToFile() (err error) {
|
|
availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)
|
|
|
|
file, err := json.MarshalIndent(p, "", " ")
|
|
if err != nil {
|
|
fs.Logf(nil, "%s", err)
|
|
}
|
|
err = os.WriteFile(availablePluginsJSON, file, 0755)
|
|
if err != nil {
|
|
fs.Logf(nil, "%s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugins) removePlugin(name string) (err error) {
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
err = p.readFromFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, ok := p.LoadedPlugins[name]
|
|
if !ok {
|
|
return fmt.Errorf("plugin %s not loaded", name)
|
|
}
|
|
delete(p.LoadedPlugins, name)
|
|
|
|
err = p.writeToFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPluginByName returns the plugin object for the key (author/plugin-name)
|
|
func (p *Plugins) GetPluginByName(name string) (out *PackageJSON, err error) {
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
po, ok := p.LoadedPlugins[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("plugin %s not loaded", name)
|
|
}
|
|
return &po, nil
|
|
|
|
}
|
|
|
|
// getAuthorRepoBranchGitHub gives author, repoName and branch from a github.com url
|
|
//
|
|
// url examples:
|
|
// https://github.com/rclone/rclone-webui-react/
|
|
// http://github.com/rclone/rclone-webui-react
|
|
// https://github.com/rclone/rclone-webui-react/tree/caman-js
|
|
// github.com/rclone/rclone-webui-react
|
|
func getAuthorRepoBranchGitHub(url string) (author string, repoName string, branch string, err error) {
|
|
repoURL := url
|
|
repoURL = strings.Replace(repoURL, "https://", "", 1)
|
|
repoURL = strings.Replace(repoURL, "http://", "", 1)
|
|
|
|
urlSplits := strings.Split(repoURL, "/")
|
|
|
|
if len(urlSplits) < 3 || len(urlSplits) > 5 || urlSplits[0] != "github.com" {
|
|
return "", "", "", fmt.Errorf("invalid github url: %s", url)
|
|
}
|
|
|
|
// get branch name
|
|
if len(urlSplits) == 5 && urlSplits[3] == "tree" {
|
|
return urlSplits[1], urlSplits[2], urlSplits[4], nil
|
|
}
|
|
|
|
return urlSplits[1], urlSplits[2], "master", nil
|
|
}
|
|
|
|
func filterPlugins(plugins *Plugins, compare func(packageJSON *PackageJSON) bool) map[string]PackageJSON {
|
|
output := map[string]PackageJSON{}
|
|
|
|
for key, val := range plugins.LoadedPlugins {
|
|
if compare(&val) {
|
|
output[key] = val
|
|
}
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// getDirectorForProxy is a helper function for reverse proxy of test plugins
|
|
func getDirectorForProxy(origin *url.URL) func(req *http.Request) {
|
|
return func(req *http.Request) {
|
|
req.Header.Add("X-Forwarded-Host", req.Host)
|
|
req.Header.Add("X-Origin-Host", origin.Host)
|
|
req.URL.Scheme = "http"
|
|
req.URL.Host = origin.Host
|
|
req.URL.Path = origin.Path
|
|
}
|
|
}
|
|
|
|
// ServePluginOK checks the plugin url and uses reverse proxy to allow redirection for content not being served by rclone
|
|
func ServePluginOK(w http.ResponseWriter, r *http.Request, pluginsMatchResult []string) (ok bool) {
|
|
testPlugin, err := loadedPlugins.GetPluginByName(fmt.Sprintf("%s/%s", pluginsMatchResult[1], pluginsMatchResult[2]))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !testPlugin.Rclone.Test {
|
|
return false
|
|
}
|
|
origin, _ := url.Parse(fmt.Sprintf("%s/%s", testPlugin.TestURL, pluginsMatchResult[3]))
|
|
|
|
director := getDirectorForProxy(origin)
|
|
|
|
pluginsProxy.Director = director
|
|
pluginsProxy.ServeHTTP(w, r)
|
|
return true
|
|
}
|
|
|
|
var referrerPathReg = regexp.MustCompile(`^(https?):\/\/(.+):([0-9]+)?\/(.*)\/?\?(.*)$`)
|
|
|
|
// ServePluginWithReferrerOK check if redirectReferrer is set for the referred a plugin, if yes,
|
|
// sends a redirect to actual url. This function is useful for plugins to refer to absolute paths when
|
|
// the referrer in http.Request is set
|
|
func ServePluginWithReferrerOK(w http.ResponseWriter, r *http.Request, path string) (ok bool) {
|
|
err := initPluginsOrError()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
referrer := r.Referer()
|
|
referrerPathMatch := referrerPathReg.FindStringSubmatch(referrer)
|
|
|
|
if len(referrerPathMatch) > 3 {
|
|
referrerPluginMatch := PluginsMatch.FindStringSubmatch(referrerPathMatch[4])
|
|
if len(referrerPluginMatch) > 2 {
|
|
pluginKey := fmt.Sprintf("%s/%s", referrerPluginMatch[1], referrerPluginMatch[2])
|
|
currentPlugin, err := loadedPlugins.GetPluginByName(pluginKey)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if currentPlugin.Rclone.RedirectReferrer {
|
|
path = fmt.Sprintf("/plugins/%s/%s/%s", referrerPluginMatch[1], referrerPluginMatch[2], path)
|
|
|
|
http.Redirect(w, r, path, http.StatusMovedPermanently)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|