github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/webgui/plugins.go (about) 1 // Package webgui provides plugin functionality to the Web GUI. 2 package webgui 3 4 import ( 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/http/httputil" 10 "net/url" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 "sync" 16 17 "github.com/rclone/rclone/fs" 18 "github.com/rclone/rclone/fs/config" 19 "github.com/rclone/rclone/fs/rc/rcflags" 20 ) 21 22 // PackageJSON is the structure of package.json of a plugin 23 type PackageJSON struct { 24 Name string `json:"name"` 25 Version string `json:"version"` 26 Description string `json:"description"` 27 Author string `json:"author"` 28 Copyright string `json:"copyright"` 29 License string `json:"license"` 30 Private bool `json:"private"` 31 Homepage string `json:"homepage"` 32 TestURL string `json:"testUrl"` 33 Repository struct { 34 Type string `json:"type"` 35 URL string `json:"url"` 36 } `json:"repository"` 37 Bugs struct { 38 URL string `json:"url"` 39 } `json:"bugs"` 40 Rclone RcloneConfig `json:"rclone"` 41 } 42 43 // RcloneConfig represents the rclone specific config 44 type RcloneConfig struct { 45 HandlesType []string `json:"handlesType"` 46 PluginType string `json:"pluginType"` 47 RedirectReferrer bool `json:"redirectReferrer"` 48 Test bool `json:"-"` 49 } 50 51 func (r *PackageJSON) isTesting() bool { 52 return r.Rclone.Test 53 } 54 55 var ( 56 //loadedTestPlugins *Plugins 57 cachePath string 58 59 loadedPlugins *Plugins 60 pluginsProxy = &httputil.ReverseProxy{} 61 // PluginsMatch is used for matching author and plugin name in the url path 62 PluginsMatch = regexp.MustCompile(`^plugins\/([^\/]*)\/([^\/\?]+)[\/]?(.*)$`) 63 // PluginsPath is the base path where webgui plugins are stored 64 PluginsPath string 65 pluginsConfigPath string 66 availablePluginsJSONPath = "availablePlugins.json" 67 initSuccess = false 68 initMutex = &sync.Mutex{} 69 ) 70 71 // Plugins represents the structure how plugins are saved onto disk 72 type Plugins struct { 73 mutex sync.Mutex 74 LoadedPlugins map[string]PackageJSON `json:"loadedPlugins"` 75 fileName string 76 } 77 78 func newPlugins(fileName string) *Plugins { 79 p := Plugins{LoadedPlugins: map[string]PackageJSON{}} 80 p.fileName = fileName 81 p.mutex = sync.Mutex{} 82 return &p 83 } 84 85 func initPluginsOrError() error { 86 if !rcflags.Opt.WebUI { 87 return errors.New("WebUI needs to be enabled for plugins to work") 88 } 89 initMutex.Lock() 90 defer initMutex.Unlock() 91 if !initSuccess { 92 cachePath = filepath.Join(config.GetCacheDir(), "webgui") 93 PluginsPath = filepath.Join(cachePath, "plugins") 94 pluginsConfigPath = filepath.Join(PluginsPath, "config") 95 loadedPlugins = newPlugins(availablePluginsJSONPath) 96 err := loadedPlugins.readFromFile() 97 if err != nil { 98 fs.Errorf(nil, "error reading available plugins: %v", err) 99 } 100 initSuccess = true 101 } 102 103 return nil 104 } 105 106 func (p *Plugins) readFromFile() (err error) { 107 err = CreatePathIfNotExist(pluginsConfigPath) 108 if err != nil { 109 return err 110 } 111 availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName) 112 _, err = os.Stat(availablePluginsJSON) 113 if err == nil { 114 data, err := os.ReadFile(availablePluginsJSON) 115 if err != nil { 116 return err 117 } 118 err = json.Unmarshal(data, &p) 119 if err != nil { 120 fs.Logf(nil, "%s", err) 121 } 122 return nil 123 } else if os.IsNotExist(err) { 124 // path does not exist 125 err = p.writeToFile() 126 if err != nil { 127 return err 128 } 129 } 130 return nil 131 } 132 133 func (p *Plugins) addPlugin(pluginName string, packageJSONPath string) (err error) { 134 p.mutex.Lock() 135 defer p.mutex.Unlock() 136 data, err := os.ReadFile(packageJSONPath) 137 if err != nil { 138 return err 139 } 140 var pkgJSON = PackageJSON{} 141 err = json.Unmarshal(data, &pkgJSON) 142 if err != nil { 143 return err 144 } 145 p.LoadedPlugins[pluginName] = pkgJSON 146 147 err = p.writeToFile() 148 if err != nil { 149 return err 150 } 151 152 return nil 153 } 154 155 func (p *Plugins) writeToFile() (err error) { 156 availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName) 157 158 file, err := json.MarshalIndent(p, "", " ") 159 if err != nil { 160 fs.Logf(nil, "%s", err) 161 } 162 err = os.WriteFile(availablePluginsJSON, file, 0755) 163 if err != nil { 164 fs.Logf(nil, "%s", err) 165 } 166 return nil 167 } 168 169 func (p *Plugins) removePlugin(name string) (err error) { 170 p.mutex.Lock() 171 defer p.mutex.Unlock() 172 err = p.readFromFile() 173 if err != nil { 174 return err 175 } 176 177 _, ok := p.LoadedPlugins[name] 178 if !ok { 179 return fmt.Errorf("plugin %s not loaded", name) 180 } 181 delete(p.LoadedPlugins, name) 182 183 err = p.writeToFile() 184 if err != nil { 185 return err 186 } 187 return nil 188 } 189 190 // GetPluginByName returns the plugin object for the key (author/plugin-name) 191 func (p *Plugins) GetPluginByName(name string) (out *PackageJSON, err error) { 192 p.mutex.Lock() 193 defer p.mutex.Unlock() 194 po, ok := p.LoadedPlugins[name] 195 if !ok { 196 return nil, fmt.Errorf("plugin %s not loaded", name) 197 } 198 return &po, nil 199 200 } 201 202 // getAuthorRepoBranchGitHub gives author, repoName and branch from a github.com url 203 // 204 // url examples: 205 // https://github.com/rclone/rclone-webui-react/ 206 // http://github.com/rclone/rclone-webui-react 207 // https://github.com/rclone/rclone-webui-react/tree/caman-js 208 // github.com/rclone/rclone-webui-react 209 func getAuthorRepoBranchGitHub(url string) (author string, repoName string, branch string, err error) { 210 repoURL := url 211 repoURL = strings.Replace(repoURL, "https://", "", 1) 212 repoURL = strings.Replace(repoURL, "http://", "", 1) 213 214 urlSplits := strings.Split(repoURL, "/") 215 216 if len(urlSplits) < 3 || len(urlSplits) > 5 || urlSplits[0] != "github.com" { 217 return "", "", "", fmt.Errorf("invalid github url: %s", url) 218 } 219 220 // get branch name 221 if len(urlSplits) == 5 && urlSplits[3] == "tree" { 222 return urlSplits[1], urlSplits[2], urlSplits[4], nil 223 } 224 225 return urlSplits[1], urlSplits[2], "master", nil 226 } 227 228 func filterPlugins(plugins *Plugins, compare func(packageJSON *PackageJSON) bool) map[string]PackageJSON { 229 output := map[string]PackageJSON{} 230 231 for key, val := range plugins.LoadedPlugins { 232 if compare(&val) { 233 output[key] = val 234 } 235 } 236 237 return output 238 } 239 240 // getDirectorForProxy is a helper function for reverse proxy of test plugins 241 func getDirectorForProxy(origin *url.URL) func(req *http.Request) { 242 return func(req *http.Request) { 243 req.Header.Add("X-Forwarded-Host", req.Host) 244 req.Header.Add("X-Origin-Host", origin.Host) 245 req.URL.Scheme = "http" 246 req.URL.Host = origin.Host 247 req.URL.Path = origin.Path 248 } 249 } 250 251 // ServePluginOK checks the plugin url and uses reverse proxy to allow redirection for content not being served by rclone 252 func ServePluginOK(w http.ResponseWriter, r *http.Request, pluginsMatchResult []string) (ok bool) { 253 testPlugin, err := loadedPlugins.GetPluginByName(fmt.Sprintf("%s/%s", pluginsMatchResult[1], pluginsMatchResult[2])) 254 if err != nil { 255 return false 256 } 257 if !testPlugin.Rclone.Test { 258 return false 259 } 260 origin, _ := url.Parse(fmt.Sprintf("%s/%s", testPlugin.TestURL, pluginsMatchResult[3])) 261 262 director := getDirectorForProxy(origin) 263 264 pluginsProxy.Director = director 265 pluginsProxy.ServeHTTP(w, r) 266 return true 267 } 268 269 var referrerPathReg = regexp.MustCompile(`^(https?):\/\/(.+):([0-9]+)?\/(.*)\/?\?(.*)$`) 270 271 // ServePluginWithReferrerOK check if redirectReferrer is set for the referred a plugin, if yes, 272 // sends a redirect to actual url. This function is useful for plugins to refer to absolute paths when 273 // the referrer in http.Request is set 274 func ServePluginWithReferrerOK(w http.ResponseWriter, r *http.Request, path string) (ok bool) { 275 err := initPluginsOrError() 276 if err != nil { 277 return false 278 } 279 referrer := r.Referer() 280 referrerPathMatch := referrerPathReg.FindStringSubmatch(referrer) 281 282 if len(referrerPathMatch) > 3 { 283 referrerPluginMatch := PluginsMatch.FindStringSubmatch(referrerPathMatch[4]) 284 if len(referrerPluginMatch) > 2 { 285 pluginKey := fmt.Sprintf("%s/%s", referrerPluginMatch[1], referrerPluginMatch[2]) 286 currentPlugin, err := loadedPlugins.GetPluginByName(pluginKey) 287 if err != nil { 288 return false 289 } 290 if currentPlugin.Rclone.RedirectReferrer { 291 path = fmt.Sprintf("/plugins/%s/%s/%s", referrerPluginMatch[1], referrerPluginMatch[2], path) 292 293 http.Redirect(w, r, path, http.StatusMovedPermanently) 294 return true 295 } 296 } 297 } 298 return false 299 }