2020-04-20 18:36:34 +02:00
package plugins
import (
zipa "archive/zip"
"context"
"crypto/sha256"
2023-11-17 01:50:06 +01:00
"encoding/hex"
2020-04-20 18:36:34 +02:00
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
2022-11-23 11:42:04 +01:00
"github.com/hashicorp/go-retryablehttp"
"github.com/rs/zerolog/log"
2023-02-03 15:24:05 +01:00
"github.com/traefik/traefik/v3/pkg/logs"
2020-04-20 18:36:34 +02:00
"golang.org/x/mod/module"
"golang.org/x/mod/zip"
"gopkg.in/yaml.v3"
)
const (
sourcesFolder = "sources"
archivesFolder = "archives"
stateFilename = "state.json"
goPathSrc = "src"
pluginManifest = ".traefik.yml"
)
2022-09-14 10:56:08 +02:00
const pluginsURL = "https://plugins.traefik.io/public/"
2020-04-20 18:36:34 +02:00
const (
2022-11-25 10:50:06 +01:00
hashHeader = "X-Plugin-Hash"
2020-04-20 18:36:34 +02:00
)
2022-09-14 10:56:08 +02:00
// ClientOptions the options of a Traefik plugins client.
2020-04-20 18:36:34 +02:00
type ClientOptions struct {
Output string
}
2022-09-14 10:56:08 +02:00
// Client a Traefik plugins client.
2020-04-20 18:36:34 +02:00
type Client struct {
HTTPClient * http . Client
baseURL * url . URL
archives string
stateFile string
goPath string
sources string
}
2022-09-14 10:56:08 +02:00
// NewClient creates a new Traefik plugins client.
2020-04-20 18:36:34 +02:00
func NewClient ( opts ClientOptions ) ( * Client , error ) {
2022-09-14 10:56:08 +02:00
baseURL , err := url . Parse ( pluginsURL )
2020-04-20 18:36:34 +02:00
if err != nil {
return nil , err
}
sourcesRootPath := filepath . Join ( filepath . FromSlash ( opts . Output ) , sourcesFolder )
err = resetDirectory ( sourcesRootPath )
if err != nil {
return nil , err
}
2021-03-04 20:08:03 +01:00
goPath , err := os . MkdirTemp ( sourcesRootPath , "gop-*" )
2020-04-20 18:36:34 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to create GoPath: %w" , err )
}
archivesPath := filepath . Join ( filepath . FromSlash ( opts . Output ) , archivesFolder )
err = os . MkdirAll ( archivesPath , 0 o755 )
if err != nil {
return nil , fmt . Errorf ( "failed to create archives directory %s: %w" , archivesPath , err )
}
2022-11-23 11:42:04 +01:00
client := retryablehttp . NewClient ( )
client . Logger = logs . NewRetryableHTTPLogger ( log . Logger )
client . HTTPClient = & http . Client { Timeout : 10 * time . Second }
client . RetryMax = 3
2020-04-20 18:36:34 +02:00
return & Client {
2022-11-23 11:42:04 +01:00
HTTPClient : client . StandardClient ( ) ,
2020-04-20 18:36:34 +02:00
baseURL : baseURL ,
archives : archivesPath ,
stateFile : filepath . Join ( archivesPath , stateFilename ) ,
goPath : goPath ,
sources : filepath . Join ( goPath , goPathSrc ) ,
} , nil
}
// GoPath gets the plugins GoPath.
func ( c * Client ) GoPath ( ) string {
return c . goPath
}
// ReadManifest reads a plugin manifest.
func ( c * Client ) ReadManifest ( moduleName string ) ( * Manifest , error ) {
return ReadManifest ( c . goPath , moduleName )
}
// ReadManifest reads a plugin manifest.
func ReadManifest ( goPath , moduleName string ) ( * Manifest , error ) {
p := filepath . Join ( goPath , goPathSrc , filepath . FromSlash ( moduleName ) , pluginManifest )
file , err := os . Open ( p )
if err != nil {
return nil , fmt . Errorf ( "failed to open the plugin manifest %s: %w" , p , err )
}
defer func ( ) { _ = file . Close ( ) } ( )
m := & Manifest { }
err = yaml . NewDecoder ( file ) . Decode ( m )
if err != nil {
return nil , fmt . Errorf ( "failed to decode the plugin manifest %s: %w" , p , err )
}
return m , nil
}
// Download downloads a plugin archive.
func ( c * Client ) Download ( ctx context . Context , pName , pVersion string ) ( string , error ) {
filename := c . buildArchivePath ( pName , pVersion )
var hash string
_ , err := os . Stat ( filename )
if err != nil && ! os . IsNotExist ( err ) {
return "" , fmt . Errorf ( "failed to read archive %s: %w" , filename , err )
}
if err == nil {
hash , err = computeHash ( filename )
if err != nil {
return "" , fmt . Errorf ( "failed to compute hash: %w" , err )
}
}
endpoint , err := c . baseURL . Parse ( path . Join ( c . baseURL . Path , "download" , pName , pVersion ) )
if err != nil {
return "" , fmt . Errorf ( "failed to parse endpoint URL: %w" , err )
}
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , endpoint . String ( ) , nil )
if err != nil {
return "" , fmt . Errorf ( "failed to create request: %w" , err )
}
if hash != "" {
req . Header . Set ( hashHeader , hash )
}
resp , err := c . HTTPClient . Do ( req )
if err != nil {
return "" , fmt . Errorf ( "failed to call service: %w" , err )
}
defer func ( ) { _ = resp . Body . Close ( ) } ( )
2022-09-02 11:44:08 +02:00
switch resp . StatusCode {
case http . StatusNotModified :
// noop
return hash , nil
case http . StatusOK :
2020-04-20 18:36:34 +02:00
err = os . MkdirAll ( filepath . Dir ( filename ) , 0 o755 )
if err != nil {
return "" , fmt . Errorf ( "failed to create directory: %w" , err )
}
var file * os . File
file , err = os . Create ( filename )
if err != nil {
return "" , fmt . Errorf ( "failed to create file %q: %w" , filename , err )
}
defer func ( ) { _ = file . Close ( ) } ( )
_ , err = io . Copy ( file , resp . Body )
if err != nil {
return "" , fmt . Errorf ( "failed to write response: %w" , err )
}
hash , err = computeHash ( filename )
if err != nil {
return "" , fmt . Errorf ( "failed to compute hash: %w" , err )
}
return hash , nil
2022-09-02 11:44:08 +02:00
default :
data , _ := io . ReadAll ( resp . Body )
return "" , fmt . Errorf ( "error: %d: %s" , resp . StatusCode , string ( data ) )
2020-04-20 18:36:34 +02:00
}
}
// Check checks the plugin archive integrity.
func ( c * Client ) Check ( ctx context . Context , pName , pVersion , hash string ) error {
endpoint , err := c . baseURL . Parse ( path . Join ( c . baseURL . Path , "validate" , pName , pVersion ) )
if err != nil {
return fmt . Errorf ( "failed to parse endpoint URL: %w" , err )
}
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , endpoint . String ( ) , nil )
if err != nil {
return fmt . Errorf ( "failed to create request: %w" , err )
}
if hash != "" {
req . Header . Set ( hashHeader , hash )
}
resp , err := c . HTTPClient . Do ( req )
if err != nil {
return fmt . Errorf ( "failed to call service: %w" , err )
}
defer func ( ) { _ = resp . Body . Close ( ) } ( )
if resp . StatusCode == http . StatusOK {
return nil
}
2024-02-07 17:14:07 +01:00
return errors . New ( "plugin integrity check failed" )
2020-04-20 18:36:34 +02:00
}
// Unzip unzip a plugin archive.
func ( c * Client ) Unzip ( pName , pVersion string ) error {
err := c . unzipModule ( pName , pVersion )
if err == nil {
return nil
}
return c . unzipArchive ( pName , pVersion )
}
func ( c * Client ) unzipModule ( pName , pVersion string ) error {
src := c . buildArchivePath ( pName , pVersion )
dest := filepath . Join ( c . sources , filepath . FromSlash ( pName ) )
return zip . Unzip ( dest , module . Version { Path : pName , Version : pVersion } , src )
}
func ( c * Client ) unzipArchive ( pName , pVersion string ) error {
zipPath := c . buildArchivePath ( pName , pVersion )
archive , err := zipa . OpenReader ( zipPath )
if err != nil {
return err
}
defer func ( ) { _ = archive . Close ( ) } ( )
dest := filepath . Join ( c . sources , filepath . FromSlash ( pName ) )
for _ , f := range archive . File {
err = unzipFile ( f , dest )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to unzip %s: %w" , f . Name , err )
2020-04-20 18:36:34 +02:00
}
}
return nil
}
func unzipFile ( f * zipa . File , dest string ) error {
rc , err := f . Open ( )
if err != nil {
return err
}
defer func ( ) { _ = rc . Close ( ) } ( )
2021-05-17 07:10:09 -03:00
pathParts := strings . SplitN ( f . Name , "/" , 2 )
2023-11-30 22:42:06 +02:00
var pp string
if len ( pathParts ) < 2 {
pp = pathParts [ 0 ]
} else {
pp = pathParts [ 1 ]
}
p := filepath . Join ( dest , pp )
2020-04-20 18:36:34 +02:00
if f . FileInfo ( ) . IsDir ( ) {
2023-06-02 11:34:06 +02:00
err = os . MkdirAll ( p , f . Mode ( ) )
if err != nil {
return fmt . Errorf ( "unable to create archive directory %s: %w" , p , err )
}
return nil
2020-04-20 18:36:34 +02:00
}
err = os . MkdirAll ( filepath . Dir ( p ) , 0 o750 )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to create archive directory %s for file %s: %w" , filepath . Dir ( p ) , p , err )
2020-04-20 18:36:34 +02:00
}
elt , err := os . OpenFile ( p , os . O_WRONLY | os . O_CREATE | os . O_TRUNC , f . Mode ( ) )
if err != nil {
return err
}
defer func ( ) { _ = elt . Close ( ) } ( )
_ , err = io . Copy ( elt , rc )
if err != nil {
return err
}
return nil
}
// CleanArchives cleans plugins archives.
func ( c * Client ) CleanArchives ( plugins map [ string ] Descriptor ) error {
if _ , err := os . Stat ( c . stateFile ) ; os . IsNotExist ( err ) {
return nil
}
stateFile , err := os . Open ( c . stateFile )
if err != nil {
return fmt . Errorf ( "failed to open state file %s: %w" , c . stateFile , err )
}
previous := make ( map [ string ] string )
err = json . NewDecoder ( stateFile ) . Decode ( & previous )
if err != nil {
return fmt . Errorf ( "failed to decode state file %s: %w" , c . stateFile , err )
}
for pName , pVersion := range previous {
for _ , desc := range plugins {
if desc . ModuleName == pName && desc . Version != pVersion {
archivePath := c . buildArchivePath ( pName , pVersion )
if err = os . RemoveAll ( archivePath ) ; err != nil {
return fmt . Errorf ( "failed to remove archive %s: %w" , archivePath , err )
}
}
}
}
return nil
}
// WriteState writes the plugins state files.
func ( c * Client ) WriteState ( plugins map [ string ] Descriptor ) error {
m := make ( map [ string ] string )
for _ , descriptor := range plugins {
m [ descriptor . ModuleName ] = descriptor . Version
}
mp , err := json . MarshalIndent ( m , "" , " " )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to marshal plugin state: %w" , err )
2020-04-20 18:36:34 +02:00
}
2021-03-04 20:08:03 +01:00
return os . WriteFile ( c . stateFile , mp , 0 o600 )
2020-04-20 18:36:34 +02:00
}
// ResetAll resets all plugins related directories.
func ( c * Client ) ResetAll ( ) error {
if c . goPath == "" {
return errors . New ( "goPath is empty" )
}
err := resetDirectory ( filepath . Join ( c . goPath , ".." ) )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to reset plugins GoPath directory %s: %w" , c . goPath , err )
}
err = resetDirectory ( c . archives )
if err != nil {
return fmt . Errorf ( "unable to reset plugins archives directory: %w" , err )
2020-04-20 18:36:34 +02:00
}
2023-06-02 11:34:06 +02:00
return nil
2020-04-20 18:36:34 +02:00
}
func ( c * Client ) buildArchivePath ( pName , pVersion string ) string {
return filepath . Join ( c . archives , filepath . FromSlash ( pName ) , pVersion + ".zip" )
}
func resetDirectory ( dir string ) error {
dirPath , err := filepath . Abs ( dir )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to get absolute path of %s: %w" , dir , err )
2020-04-20 18:36:34 +02:00
}
currentPath , err := os . Getwd ( )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to get the current directory: %w" , err )
2020-04-20 18:36:34 +02:00
}
if strings . HasPrefix ( currentPath , dirPath ) {
return fmt . Errorf ( "cannot be deleted: the directory path %s is the parent of the current path %s" , dirPath , currentPath )
}
err = os . RemoveAll ( dir )
if err != nil {
2023-06-02 11:34:06 +02:00
return fmt . Errorf ( "unable to remove directory %s: %w" , dirPath , err )
2020-04-20 18:36:34 +02:00
}
2023-06-02 11:34:06 +02:00
err = os . MkdirAll ( dir , 0 o755 )
if err != nil {
return fmt . Errorf ( "unable to create directory %s: %w" , dirPath , err )
}
return nil
2020-04-20 18:36:34 +02:00
}
func computeHash ( filepath string ) ( string , error ) {
file , err := os . Open ( filepath )
if err != nil {
return "" , err
}
hash := sha256 . New ( )
if _ , err := io . Copy ( hash , file ) ; err != nil {
return "" , err
}
sum := hash . Sum ( nil )
2023-11-17 01:50:06 +01:00
return hex . EncodeToString ( sum ) , nil
2020-04-20 18:36:34 +02:00
}