rclone/fs/rc/webgui/plugins.go
Nick Craig-Wood e43b5ce5e5 Remove github.com/pkg/errors and replace with std library version
This is possible now that we no longer support go1.12 and brings
rclone into line with standard practices in the Go world.

This also removes errors.New and errors.Errorf from lib/errors and
prefers the stdlib errors package over lib/errors.
2021-11-07 11:53:30 +00:00

326 lines
8.4 KiB
Go

package webgui
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"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 := ioutil.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 := ioutil.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 = ioutil.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 referrerPathMatch != nil && len(referrerPathMatch) > 3 {
referrerPluginMatch := PluginsMatch.FindStringSubmatch(referrerPathMatch[4])
if referrerPluginMatch != nil && 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
}