2016-02-15 21:09:34 +08:00
package commands
import (
"archive/zip"
"bytes"
"errors"
2016-03-10 21:43:21 +08:00
"fmt"
2016-02-15 21:09:34 +08:00
"io"
2019-11-06 20:42:58 +08:00
"io/ioutil"
2016-02-15 21:09:34 +08:00
"os"
"path"
2019-07-29 16:44:58 +08:00
"path/filepath"
2016-02-15 21:09:34 +08:00
"regexp"
2019-07-29 16:44:58 +08:00
"runtime"
2016-03-13 18:29:43 +08:00
"strings"
2016-03-29 03:42:26 +08:00
"github.com/fatih/color"
2019-05-27 16:47:21 +08:00
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
2019-07-29 16:44:58 +08:00
"github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/xerrors"
2019-04-23 16:24:47 +08:00
2016-06-03 18:19:04 +08:00
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
2016-03-29 03:42:26 +08:00
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
2016-02-15 21:09:34 +08:00
)
2019-05-27 16:47:21 +08:00
func validateInput ( c utils . CommandLine , pluginFolder string ) error {
2016-02-15 21:09:34 +08:00
arg := c . Args ( ) . First ( )
if arg == "" {
return errors . New ( "please specify plugin to install" )
}
2016-06-25 02:14:58 +08:00
pluginsDir := c . PluginDirectory ( )
2016-03-29 03:42:26 +08:00
if pluginsDir == "" {
return errors . New ( "missing pluginsDir flag" )
2016-02-15 21:09:34 +08:00
}
2016-03-29 03:42:26 +08:00
fileInfo , err := os . Stat ( pluginsDir )
2016-03-11 21:11:25 +08:00
if err != nil {
2016-03-29 03:42:26 +08:00
if err = os . MkdirAll ( pluginsDir , os . ModePerm ) ; err != nil {
2018-04-17 02:25:48 +08:00
return fmt . Errorf ( "pluginsDir (%s) is not a writable directory" , pluginsDir )
2016-03-11 21:11:25 +08:00
}
return nil
}
if ! fileInfo . IsDir ( ) {
2016-02-15 21:09:34 +08:00
return errors . New ( "path is not a directory" )
}
return nil
}
2019-05-27 16:47:21 +08:00
func installCommand ( c utils . CommandLine ) error {
2016-06-25 02:14:58 +08:00
pluginFolder := c . PluginDirectory ( )
2016-02-15 21:09:34 +08:00
if err := validateInput ( c , pluginFolder ) ; err != nil {
return err
}
pluginToInstall := c . Args ( ) . First ( )
version := c . Args ( ) . Get ( 1 )
2016-03-08 21:53:27 +08:00
return InstallPlugin ( pluginToInstall , version , c )
2016-02-15 21:09:34 +08:00
}
2019-02-18 20:51:43 +08:00
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
// and then extracts the zip into the plugins directory.
2019-05-27 16:47:21 +08:00
func InstallPlugin ( pluginName , version string , c utils . CommandLine ) error {
2016-06-25 02:14:58 +08:00
pluginFolder := c . PluginDirectory ( )
2017-09-16 02:34:08 +08:00
downloadURL := c . PluginURL ( )
2019-07-29 16:44:58 +08:00
isInternal := false
var checksum string
2017-09-16 02:34:08 +08:00
if downloadURL == "" {
2019-07-29 16:44:58 +08:00
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 ( ) )
2017-09-16 02:34:08 +08:00
if err != nil {
return err
}
2016-02-15 21:09:34 +08:00
2019-07-29 16:44:58 +08:00
v , err := SelectVersion ( & plugin , version )
2017-09-16 02:34:08 +08:00
if err != nil {
return err
}
2016-02-15 21:09:34 +08:00
2017-09-16 02:34:08 +08:00
if version == "" {
version = v . Version
}
downloadURL = fmt . Sprintf ( "%s/%s/versions/%s/download" ,
c . GlobalString ( "repo" ) ,
pluginName ,
2019-07-29 16:44:58 +08:00
version ,
)
// Plugins which are downloaded just as sourcecode zipball from github do not have checksum
if v . Arch != nil {
checksum = v . Arch [ osAndArchString ( ) ] . Md5
}
2016-03-07 23:06:35 +08:00
}
2017-09-16 02:34:08 +08:00
logger . Infof ( "installing %v @ %v\n" , pluginName , version )
2019-07-10 15:40:33 +08:00
logger . Infof ( "from: %v\n" , downloadURL )
2016-06-03 18:19:04 +08:00
logger . Infof ( "into: %v\n" , pluginFolder )
logger . Info ( "\n" )
2016-02-15 21:09:34 +08:00
2019-11-06 20:42:58 +08:00
// Create temp file for downloading zip file
tmpFile , err := ioutil . TempFile ( "" , "*.zip" )
2016-03-10 21:43:21 +08:00
if err != nil {
2019-11-06 20:42:58 +08:00
return errutil . Wrap ( "Failed to create temporary file" , err )
}
defer os . Remove ( tmpFile . Name ( ) )
err = c . ApiClient ( ) . DownloadFile ( pluginName , tmpFile , downloadURL , checksum )
if err != nil {
tmpFile . Close ( )
2019-07-29 16:44:58 +08:00
return errutil . Wrap ( "Failed to download plugin archive" , err )
}
2019-11-06 20:42:58 +08:00
err = tmpFile . Close ( )
if err != nil {
return errutil . Wrap ( "Failed to close tmp file" , err )
}
2019-07-29 16:44:58 +08:00
2019-11-06 20:42:58 +08:00
err = extractFiles ( tmpFile . Name ( ) , pluginName , pluginFolder , isInternal )
2019-07-29 16:44:58 +08:00
if err != nil {
return errutil . Wrap ( "Failed to extract plugin archive" , err )
2016-02-15 21:09:34 +08:00
}
2017-09-16 02:34:08 +08:00
logger . Infof ( "%s Installed %s successfully \n" , color . GreenString ( "✔" ) , pluginName )
2018-02-16 16:49:29 +08:00
res , _ := s . ReadPlugin ( pluginFolder , pluginName )
for _ , v := range res . Dependencies . Plugins {
2019-10-15 22:44:15 +08:00
if err := InstallPlugin ( v . Id , "" , c ) ; err != nil {
return errutil . Wrapf ( err , "Failed to install plugin '%s'" , v . Id )
}
2018-02-16 16:49:29 +08:00
logger . Infof ( "Installed dependency: %v ✔\n" , v . Id )
}
return err
2016-02-15 21:09:34 +08:00
}
2019-07-29 16:44:58 +08:00
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
2019-09-30 21:34:09 +08:00
// os and platform. It expects plugin.Versions to be sorted so the newest version is first.
2019-07-29 16:44:58 +08:00
func SelectVersion ( plugin * m . Plugin , version string ) ( * m . Version , error ) {
2019-09-30 21:34:09 +08:00
var ver m . Version
latestForArch := latestSupportedVersion ( plugin )
if latestForArch == nil {
return nil , xerrors . New ( "Plugin is not supported on your architecture and os." )
2016-02-15 21:09:34 +08:00
}
2019-09-30 21:34:09 +08:00
if version == "" {
return latestForArch , nil
}
2016-02-15 21:09:34 +08:00
for _ , v := range plugin . Versions {
if v . Version == version {
2019-09-30 21:34:09 +08:00
ver = v
break
2016-02-15 21:09:34 +08:00
}
}
2019-09-30 21:34:09 +08:00
if len ( ver . Version ) == 0 {
2019-07-29 16:44:58 +08:00
return nil , xerrors . New ( "Could not find the version you're looking for" )
}
2019-09-30 21:34:09 +08:00
if ! supportsCurrentArch ( & ver ) {
return nil , xerrors . Errorf ( "Version you want is not supported on your architecture and os. Latest suitable version is %v" , latestForArch . Version )
2019-07-29 16:44:58 +08:00
}
2019-09-30 21:34:09 +08:00
return & ver , nil
2016-02-15 21:09:34 +08:00
}
2016-03-08 21:30:25 +08:00
func RemoveGitBuildFromName ( pluginName , filename string ) string {
2016-02-15 21:09:34 +08:00
r := regexp . MustCompile ( "^[a-zA-Z0-9_.-]*/" )
2016-03-08 21:30:25 +08:00
return r . ReplaceAllString ( filename , pluginName + "/" )
2016-02-15 21:09:34 +08:00
}
2016-04-05 21:34:24 +08:00
var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir"
2016-03-08 20:16:04 +08:00
2019-11-06 20:42:58 +08:00
func extractFiles ( archiveFile string , pluginName string , filePath string , allowSymlinks bool ) error {
logger . Debugf ( "Extracting archive %v to %v...\n" , archiveFile , filePath )
r , err := zip . OpenReader ( archiveFile )
2016-02-15 21:09:34 +08:00
if err != nil {
return err
}
for _ , zf := range r . File {
2019-07-29 16:44:58 +08:00
newFileName := RemoveGitBuildFromName ( pluginName , zf . Name )
2019-09-14 00:12:52 +08:00
if ! isPathSafe ( newFileName , filepath . Join ( filePath , pluginName ) ) {
2019-07-29 16:44:58 +08:00
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 )
2016-02-15 21:09:34 +08:00
if zf . FileInfo ( ) . IsDir ( ) {
2019-05-01 17:36:02 +08:00
err := os . Mkdir ( newFile , 0755 )
2019-09-14 00:12:52 +08:00
if os . IsPermission ( err ) {
2016-04-05 21:34:24 +08:00
return fmt . Errorf ( permissionsDeniedMessage , newFile )
}
2016-02-15 21:09:34 +08:00
} else {
2019-07-29 16:44:58 +08:00
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 {
err = extractFile ( zf , newFile )
if err != nil {
2019-12-04 19:48:40 +08:00
return errutil . Wrap ( "Failed to extract file" , err )
2019-07-29 16:44:58 +08:00
}
2016-02-15 21:09:34 +08:00
}
}
}
return nil
}
2016-04-05 21:34:24 +08:00
2019-07-29 16:44:58 +08:00
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 {
2019-09-14 00:12:52 +08:00
if os . IsPermission ( err ) {
2019-07-29 16:44:58 +08:00
return xerrors . Errorf ( permissionsDeniedMessage , filePath )
}
2019-12-04 19:48:40 +08:00
unwrappedError := xerrors . Unwrap ( err )
if unwrappedError != nil && strings . EqualFold ( unwrappedError . Error ( ) , "text file busy" ) {
return fmt . Errorf ( "file %s is in use. Please stop Grafana, install the plugin and restart Grafana" , filePath )
}
2019-07-29 16:44:58 +08:00
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 )
}