rclone/fs/rc/webgui/plugins.go
albertony 5d6b8141ec Replace deprecated ioutil
As of Go 1.16, the same functionality is now provided by package io or
package os, and those implementations should be preferred in new code.
2022-11-07 11:41:47 +00:00

326 lines
8.4 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) addTestPlugin(pluginName string, testURL string, handlesType []string) (err error) {
p.mutex.Lock()
defer p.mutex.Unlock()
err = p.readFromFile()
if err != nil {
return err
}
var pkgJSON = PackageJSON{
Name: pluginName,
TestURL: testURL,
Rclone: RcloneConfig{
HandlesType: handlesType,
Test: true,
},
}
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
}