install_command.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. package commands
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "regexp"
  12. "runtime"
  13. "strings"
  14. "github.com/fatih/color"
  15. "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
  16. "github.com/grafana/grafana/pkg/util/errutil"
  17. "golang.org/x/xerrors"
  18. "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
  19. m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
  20. s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
  21. )
  22. func validateInput(c utils.CommandLine, pluginFolder string) error {
  23. arg := c.Args().First()
  24. if arg == "" {
  25. return errors.New("please specify plugin to install")
  26. }
  27. pluginsDir := c.PluginDirectory()
  28. if pluginsDir == "" {
  29. return errors.New("missing pluginsDir flag")
  30. }
  31. fileInfo, err := os.Stat(pluginsDir)
  32. if err != nil {
  33. if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
  34. return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
  35. }
  36. return nil
  37. }
  38. if !fileInfo.IsDir() {
  39. return errors.New("path is not a directory")
  40. }
  41. return nil
  42. }
  43. func installCommand(c utils.CommandLine) error {
  44. pluginFolder := c.PluginDirectory()
  45. if err := validateInput(c, pluginFolder); err != nil {
  46. return err
  47. }
  48. pluginToInstall := c.Args().First()
  49. version := c.Args().Get(1)
  50. return InstallPlugin(pluginToInstall, version, c)
  51. }
  52. // InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
  53. // and then extracts the zip into the plugins directory.
  54. func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
  55. pluginFolder := c.PluginDirectory()
  56. downloadURL := c.PluginURL()
  57. isInternal := false
  58. var checksum string
  59. if downloadURL == "" {
  60. if strings.HasPrefix(pluginName, "grafana-") {
  61. // At this point the plugin download is going through grafana.com API and thus the name is validated.
  62. // Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
  63. // You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
  64. // is up to the user to know what she is doing.
  65. isInternal = true
  66. }
  67. plugin, err := c.ApiClient().GetPlugin(pluginName, c.RepoDirectory())
  68. if err != nil {
  69. return err
  70. }
  71. v, err := SelectVersion(&plugin, version)
  72. if err != nil {
  73. return err
  74. }
  75. if version == "" {
  76. version = v.Version
  77. }
  78. downloadURL = fmt.Sprintf("%s/%s/versions/%s/download",
  79. c.GlobalString("repo"),
  80. pluginName,
  81. version,
  82. )
  83. // Plugins which are downloaded just as sourcecode zipball from github do not have checksum
  84. if v.Arch != nil {
  85. checksum = v.Arch[osAndArchString()].Md5
  86. }
  87. }
  88. logger.Infof("installing %v @ %v\n", pluginName, version)
  89. logger.Infof("from: %v\n", downloadURL)
  90. logger.Infof("into: %v\n", pluginFolder)
  91. logger.Info("\n")
  92. content, err := c.ApiClient().DownloadFile(pluginName, pluginFolder, downloadURL, checksum)
  93. if err != nil {
  94. return errutil.Wrap("Failed to download plugin archive", err)
  95. }
  96. err = extractFiles(content, pluginName, pluginFolder, isInternal)
  97. if err != nil {
  98. return errutil.Wrap("Failed to extract plugin archive", err)
  99. }
  100. logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName)
  101. res, _ := s.ReadPlugin(pluginFolder, pluginName)
  102. for _, v := range res.Dependencies.Plugins {
  103. InstallPlugin(v.Id, "", c)
  104. logger.Infof("Installed dependency: %v ✔\n", v.Id)
  105. }
  106. return err
  107. }
  108. func osAndArchString() string {
  109. osString := strings.ToLower(runtime.GOOS)
  110. arch := runtime.GOARCH
  111. return osString + "-" + arch
  112. }
  113. func supportsCurrentArch(version *m.Version) bool {
  114. if version.Arch == nil {
  115. return true
  116. }
  117. for arch := range version.Arch {
  118. if arch == osAndArchString() || arch == "any" {
  119. return true
  120. }
  121. }
  122. return false
  123. }
  124. func latestSupportedVersion(plugin *m.Plugin) *m.Version {
  125. for _, ver := range plugin.Versions {
  126. if supportsCurrentArch(&ver) {
  127. return &ver
  128. }
  129. }
  130. return nil
  131. }
  132. // SelectVersion returns latest version if none is specified or the specified version. If the version string is not
  133. // matched to existing version it errors out. It also errors out if version that is matched is not available for current
  134. // os and platform.
  135. func SelectVersion(plugin *m.Plugin, version string) (*m.Version, error) {
  136. var ver *m.Version
  137. if version == "" {
  138. ver = &plugin.Versions[0]
  139. }
  140. for _, v := range plugin.Versions {
  141. if v.Version == version {
  142. ver = &v
  143. }
  144. }
  145. if ver == nil {
  146. return nil, xerrors.New("Could not find the version you're looking for")
  147. }
  148. latestForArch := latestSupportedVersion(plugin)
  149. if latestForArch == nil {
  150. return nil, xerrors.New("Plugin is not supported on your architecture and os.")
  151. }
  152. if latestForArch.Version == ver.Version {
  153. return ver, nil
  154. }
  155. return nil, xerrors.Errorf("Version you want is not supported on your architecture and os. Latest suitable version is %v", latestForArch.Version)
  156. }
  157. func RemoveGitBuildFromName(pluginName, filename string) string {
  158. r := regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
  159. return r.ReplaceAllString(filename, pluginName+"/")
  160. }
  161. var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir"
  162. func extractFiles(body []byte, pluginName string, filePath string, allowSymlinks bool) error {
  163. r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
  164. if err != nil {
  165. return err
  166. }
  167. for _, zf := range r.File {
  168. newFileName := RemoveGitBuildFromName(pluginName, zf.Name)
  169. if !isPathSafe(newFileName, filepath.Join(filePath, pluginName)) {
  170. 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))
  171. }
  172. newFile := path.Join(filePath, newFileName)
  173. if zf.FileInfo().IsDir() {
  174. err := os.Mkdir(newFile, 0755)
  175. if os.IsPermission(err) {
  176. return fmt.Errorf(permissionsDeniedMessage, newFile)
  177. }
  178. } else {
  179. if isSymlink(zf) {
  180. if !allowSymlinks {
  181. logger.Errorf("%v: plugin archive contains symlink which is not allowed. Skipping \n", zf.Name)
  182. continue
  183. }
  184. err = extractSymlink(zf, newFile)
  185. if err != nil {
  186. logger.Errorf("Failed to extract symlink: %v \n", err)
  187. continue
  188. }
  189. } else {
  190. err = extractFile(zf, newFile)
  191. if err != nil {
  192. logger.Errorf("Failed to extract file: %v \n", err)
  193. continue
  194. }
  195. }
  196. }
  197. }
  198. return nil
  199. }
  200. func isSymlink(file *zip.File) bool {
  201. return file.Mode()&os.ModeSymlink == os.ModeSymlink
  202. }
  203. func extractSymlink(file *zip.File, filePath string) error {
  204. // symlink target is the contents of the file
  205. src, err := file.Open()
  206. if err != nil {
  207. return errutil.Wrap("Failed to extract file", err)
  208. }
  209. buf := new(bytes.Buffer)
  210. _, err = io.Copy(buf, src)
  211. if err != nil {
  212. return errutil.Wrap("Failed to copy symlink contents", err)
  213. }
  214. err = os.Symlink(strings.TrimSpace(buf.String()), filePath)
  215. if err != nil {
  216. return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
  217. }
  218. return nil
  219. }
  220. func extractFile(file *zip.File, filePath string) (err error) {
  221. fileMode := file.Mode()
  222. // This is entry point for backend plugins so we want to make them executable
  223. if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_darwin_amd64") {
  224. fileMode = os.FileMode(0755)
  225. }
  226. dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
  227. if err != nil {
  228. if os.IsPermission(err) {
  229. return xerrors.Errorf(permissionsDeniedMessage, filePath)
  230. }
  231. return errutil.Wrap("Failed to open file", err)
  232. }
  233. defer func() {
  234. err = dst.Close()
  235. }()
  236. src, err := file.Open()
  237. if err != nil {
  238. return errutil.Wrap("Failed to extract file", err)
  239. }
  240. defer func() {
  241. err = src.Close()
  242. }()
  243. _, err = io.Copy(dst, src)
  244. return
  245. }
  246. // isPathSafe checks if the filePath does not resolve outside of destination. This is used to prevent
  247. // https://snyk.io/research/zip-slip-vulnerability
  248. // Based on https://github.com/mholt/archiver/pull/65/files#diff-635e4219ee55ef011b2b32bba065606bR109
  249. func isPathSafe(filePath string, destination string) bool {
  250. destpath := filepath.Join(destination, filePath)
  251. return strings.HasPrefix(destpath, destination)
  252. }