|
|
@@ -6,15 +6,17 @@ import (
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
- "io/ioutil"
|
|
|
- "net/http"
|
|
|
"os"
|
|
|
"path"
|
|
|
+ "path/filepath"
|
|
|
"regexp"
|
|
|
+ "runtime"
|
|
|
"strings"
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
|
|
+ "github.com/grafana/grafana/pkg/util/errutil"
|
|
|
+ "golang.org/x/xerrors"
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
|
|
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
|
|
@@ -64,13 +66,23 @@ func installCommand(c utils.CommandLine) error {
|
|
|
func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
|
|
pluginFolder := c.PluginDirectory()
|
|
|
downloadURL := c.PluginURL()
|
|
|
+ isInternal := false
|
|
|
+
|
|
|
+ var checksum string
|
|
|
if downloadURL == "" {
|
|
|
- plugin, err := s.GetPlugin(pluginName, c.RepoDirectory())
|
|
|
+ if strings.HasPrefix(pluginName, "grafana-") {
|
|
|
+ // At this point the plugin download is going through grafana.com API and thus the name is validated.
|
|
|
+ // Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
|
|
|
+ // You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
|
|
|
+ // is up to the user to know what she is doing.
|
|
|
+ isInternal = true
|
|
|
+ }
|
|
|
+ plugin, err := c.ApiClient().GetPlugin(pluginName, c.RepoDirectory())
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- v, err := SelectVersion(plugin, version)
|
|
|
+ v, err := SelectVersion(&plugin, version)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
@@ -81,7 +93,13 @@ func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
|
|
downloadURL = fmt.Sprintf("%s/%s/versions/%s/download",
|
|
|
c.GlobalString("repo"),
|
|
|
pluginName,
|
|
|
- version)
|
|
|
+ version,
|
|
|
+ )
|
|
|
+
|
|
|
+ // Plugins which are downloaded just as sourcecode zipball from github do not have checksum
|
|
|
+ if v.Arch != nil {
|
|
|
+ checksum = v.Arch[osAndArchString()].Md5
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
logger.Infof("installing %v @ %v\n", pluginName, version)
|
|
|
@@ -89,9 +107,14 @@ func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
|
|
logger.Infof("into: %v\n", pluginFolder)
|
|
|
logger.Info("\n")
|
|
|
|
|
|
- err := downloadFile(pluginName, pluginFolder, downloadURL)
|
|
|
+ content, err := c.ApiClient().DownloadFile(pluginName, pluginFolder, downloadURL, checksum)
|
|
|
if err != nil {
|
|
|
- return err
|
|
|
+ return errutil.Wrap("Failed to download plugin archive", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ err = extractFiles(content, pluginName, pluginFolder, isInternal)
|
|
|
+ if err != nil {
|
|
|
+ return errutil.Wrap("Failed to extract plugin archive", err)
|
|
|
}
|
|
|
|
|
|
logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName)
|
|
|
@@ -105,18 +128,61 @@ func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
-func SelectVersion(plugin m.Plugin, version string) (m.Version, error) {
|
|
|
+func osAndArchString() string {
|
|
|
+ osString := strings.ToLower(runtime.GOOS)
|
|
|
+ arch := runtime.GOARCH
|
|
|
+ return osString + "-" + arch
|
|
|
+}
|
|
|
+
|
|
|
+func supportsCurrentArch(version *m.Version) bool {
|
|
|
+ if version.Arch == nil {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ for arch := range version.Arch {
|
|
|
+ if arch == osAndArchString() || arch == "any" {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+func latestSupportedVersion(plugin *m.Plugin) *m.Version {
|
|
|
+ for _, ver := range plugin.Versions {
|
|
|
+ if supportsCurrentArch(&ver) {
|
|
|
+ return &ver
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SelectVersion returns latest version if none is specified or the specified version. If the version string is not
|
|
|
+// matched to existing version it errors out. It also errors out if version that is matched is not available for current
|
|
|
+// os and platform.
|
|
|
+func SelectVersion(plugin *m.Plugin, version string) (*m.Version, error) {
|
|
|
+ var ver *m.Version
|
|
|
if version == "" {
|
|
|
- return plugin.Versions[0], nil
|
|
|
+ ver = &plugin.Versions[0]
|
|
|
}
|
|
|
|
|
|
for _, v := range plugin.Versions {
|
|
|
if v.Version == version {
|
|
|
- return v, nil
|
|
|
+ ver = &v
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return m.Version{}, errors.New("Could not find the version you're looking for")
|
|
|
+ if ver == nil {
|
|
|
+ return nil, xerrors.New("Could not find the version you're looking for")
|
|
|
+ }
|
|
|
+
|
|
|
+ latestForArch := latestSupportedVersion(plugin)
|
|
|
+ if latestForArch == nil {
|
|
|
+ return nil, xerrors.New("Plugin is not supported on your architecture and os.")
|
|
|
+ }
|
|
|
+
|
|
|
+ if latestForArch.Version == ver.Version {
|
|
|
+ return ver, nil
|
|
|
+ }
|
|
|
+ return nil, xerrors.Errorf("Version you want is not supported on your architecture and os. Latest suitable version is %v", latestForArch.Version)
|
|
|
}
|
|
|
|
|
|
func RemoveGitBuildFromName(pluginName, filename string) string {
|
|
|
@@ -124,57 +190,19 @@ func RemoveGitBuildFromName(pluginName, filename string) string {
|
|
|
return r.ReplaceAllString(filename, pluginName+"/")
|
|
|
}
|
|
|
|
|
|
-var retryCount = 0
|
|
|
var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir"
|
|
|
|
|
|
-func downloadFile(pluginName, filePath, url string) (err error) {
|
|
|
- defer func() {
|
|
|
- if r := recover(); r != nil {
|
|
|
- retryCount++
|
|
|
- if retryCount < 3 {
|
|
|
- fmt.Println("Failed downloading. Will retry once.")
|
|
|
- err = downloadFile(pluginName, filePath, url)
|
|
|
- } else {
|
|
|
- failure := fmt.Sprintf("%v", r)
|
|
|
- if failure == "runtime error: makeslice: len out of range" {
|
|
|
- err = fmt.Errorf("Corrupt http response from source. Please try again")
|
|
|
- } else {
|
|
|
- panic(r)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }()
|
|
|
-
|
|
|
- var bytes []byte
|
|
|
-
|
|
|
- if _, err := os.Stat(url); err == nil {
|
|
|
- bytes, err = ioutil.ReadFile(url)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- } else {
|
|
|
- resp, err := http.Get(url) // #nosec
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- defer resp.Body.Close()
|
|
|
-
|
|
|
- bytes, err = ioutil.ReadAll(resp.Body)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return extractFiles(bytes, pluginName, filePath)
|
|
|
-}
|
|
|
-
|
|
|
-func extractFiles(body []byte, pluginName string, filePath string) error {
|
|
|
+func extractFiles(body []byte, pluginName string, filePath string, allowSymlinks bool) error {
|
|
|
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
for _, zf := range r.File {
|
|
|
- newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name))
|
|
|
+ newFileName := RemoveGitBuildFromName(pluginName, zf.Name)
|
|
|
+ if !isPathSafe(newFileName, path.Join(filePath, pluginName)) {
|
|
|
+ return xerrors.Errorf("filepath: %v tries to write outside of plugin directory: %v. This can be a security risk.", zf.Name, path.Join(filePath, pluginName))
|
|
|
+ }
|
|
|
+ newFile := path.Join(filePath, newFileName)
|
|
|
|
|
|
if zf.FileInfo().IsDir() {
|
|
|
err := os.Mkdir(newFile, 0755)
|
|
|
@@ -182,25 +210,24 @@ func extractFiles(body []byte, pluginName string, filePath string) error {
|
|
|
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
|
|
}
|
|
|
} else {
|
|
|
- fileMode := zf.Mode()
|
|
|
-
|
|
|
- if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") {
|
|
|
- fileMode = os.FileMode(0755)
|
|
|
- }
|
|
|
-
|
|
|
- dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
|
|
- if permissionsError(err) {
|
|
|
- return fmt.Errorf(permissionsDeniedMessage, newFile)
|
|
|
- }
|
|
|
+ if isSymlink(zf) {
|
|
|
+ if !allowSymlinks {
|
|
|
+ logger.Errorf("%v: plugin archive contains symlink which is not allowed. Skipping \n", zf.Name)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ err = extractSymlink(zf, newFile)
|
|
|
+ if err != nil {
|
|
|
+ logger.Errorf("Failed to extract symlink: %v \n", err)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ } else {
|
|
|
|
|
|
- src, err := zf.Open()
|
|
|
- if err != nil {
|
|
|
- logger.Errorf("Failed to extract file: %v", err)
|
|
|
+ err = extractFile(zf, newFile)
|
|
|
+ if err != nil {
|
|
|
+ logger.Errorf("Failed to extract file: %v \n", err)
|
|
|
+ continue
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- io.Copy(dst, src)
|
|
|
- dst.Close()
|
|
|
- src.Close()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -210,3 +237,63 @@ func extractFiles(body []byte, pluginName string, filePath string) error {
|
|
|
func permissionsError(err error) bool {
|
|
|
return err != nil && strings.Contains(err.Error(), "permission denied")
|
|
|
}
|
|
|
+
|
|
|
+func isSymlink(file *zip.File) bool {
|
|
|
+ return file.Mode()&os.ModeSymlink == os.ModeSymlink
|
|
|
+}
|
|
|
+
|
|
|
+func extractSymlink(file *zip.File, filePath string) error {
|
|
|
+ // symlink target is the contents of the file
|
|
|
+ src, err := file.Open()
|
|
|
+ if err != nil {
|
|
|
+ return errutil.Wrap("Failed to extract file", err)
|
|
|
+ }
|
|
|
+ buf := new(bytes.Buffer)
|
|
|
+ _, err = io.Copy(buf, src)
|
|
|
+ if err != nil {
|
|
|
+ return errutil.Wrap("Failed to copy symlink contents", err)
|
|
|
+ }
|
|
|
+ err = os.Symlink(strings.TrimSpace(buf.String()), filePath)
|
|
|
+ if err != nil {
|
|
|
+ return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func extractFile(file *zip.File, filePath string) (err error) {
|
|
|
+ fileMode := file.Mode()
|
|
|
+ // This is entry point for backend plugins so we want to make them executable
|
|
|
+ if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_darwin_amd64") {
|
|
|
+ fileMode = os.FileMode(0755)
|
|
|
+ }
|
|
|
+
|
|
|
+ dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
|
|
+ if err != nil {
|
|
|
+ if permissionsError(err) {
|
|
|
+ return xerrors.Errorf(permissionsDeniedMessage, filePath)
|
|
|
+ }
|
|
|
+ return errutil.Wrap("Failed to open file", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ err = dst.Close()
|
|
|
+ }()
|
|
|
+
|
|
|
+ src, err := file.Open()
|
|
|
+ if err != nil {
|
|
|
+ return errutil.Wrap("Failed to extract file", err)
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ err = src.Close()
|
|
|
+ }()
|
|
|
+
|
|
|
+ _, err = io.Copy(dst, src)
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// isPathSafe checks if the filePath does not resolve outside of destination. This is used to prevent
|
|
|
+// https://snyk.io/research/zip-slip-vulnerability
|
|
|
+// Based on https://github.com/mholt/archiver/pull/65/files#diff-635e4219ee55ef011b2b32bba065606bR109
|
|
|
+func isPathSafe(filePath string, destination string) bool {
|
|
|
+ destpath := filepath.Join(destination, filePath)
|
|
|
+ return strings.HasPrefix(destpath, destination)
|
|
|
+}
|