mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 02:27:19 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			355 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			355 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package caddycmd
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"path/filepath"
 | |
| 	"reflect"
 | |
| 	"runtime"
 | |
| 	"runtime/debug"
 | |
| 	"strings"
 | |
| 
 | |
| 	"go.uber.org/zap"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| )
 | |
| 
 | |
| func cmdUpgrade(fl Flags) (int, error) {
 | |
| 	_, nonstandard, _, err := getModules()
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
 | |
| 	}
 | |
| 	pluginPkgs, err := getPluginPackages(nonstandard)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 
 | |
| 	return upgradeBuild(pluginPkgs, fl)
 | |
| }
 | |
| 
 | |
| func splitModule(arg string) (module, version string, err error) {
 | |
| 	const versionSplit = "@"
 | |
| 
 | |
| 	// accommodate module paths that have @ in them, but we can only tolerate that if there's also
 | |
| 	// a version, otherwise we don't know if it's a version separator or part of the file path
 | |
| 	lastVersionSplit := strings.LastIndex(arg, versionSplit)
 | |
| 	if lastVersionSplit < 0 {
 | |
| 		module = arg
 | |
| 	} else {
 | |
| 		module, version = arg[:lastVersionSplit], arg[lastVersionSplit+1:]
 | |
| 	}
 | |
| 
 | |
| 	if module == "" {
 | |
| 		err = fmt.Errorf("module name is required")
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func cmdAddPackage(fl Flags) (int, error) {
 | |
| 	if len(fl.Args()) == 0 {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
 | |
| 	}
 | |
| 	_, nonstandard, _, err := getModules()
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
 | |
| 	}
 | |
| 	pluginPkgs, err := getPluginPackages(nonstandard)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 
 | |
| 	for _, arg := range fl.Args() {
 | |
| 		module, version, err := splitModule(arg)
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
 | |
| 		}
 | |
| 		// only allow a version to be specified if it's different from the existing version
 | |
| 		if _, ok := pluginPkgs[module]; ok && !(version != "" && pluginPkgs[module].Version != version) {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
 | |
| 		}
 | |
| 		pluginPkgs[module] = pluginPackage{Version: version, Path: module}
 | |
| 	}
 | |
| 
 | |
| 	return upgradeBuild(pluginPkgs, fl)
 | |
| }
 | |
| 
 | |
| func cmdRemovePackage(fl Flags) (int, error) {
 | |
| 	if len(fl.Args()) == 0 {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
 | |
| 	}
 | |
| 	_, nonstandard, _, err := getModules()
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
 | |
| 	}
 | |
| 	pluginPkgs, err := getPluginPackages(nonstandard)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 
 | |
| 	for _, arg := range fl.Args() {
 | |
| 		module, _, err := splitModule(arg)
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
 | |
| 		}
 | |
| 		if _, ok := pluginPkgs[module]; !ok {
 | |
| 			// package does not exist
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
 | |
| 		}
 | |
| 		delete(pluginPkgs, arg)
 | |
| 	}
 | |
| 
 | |
| 	return upgradeBuild(pluginPkgs, fl)
 | |
| }
 | |
| 
 | |
| func upgradeBuild(pluginPkgs map[string]pluginPackage, fl Flags) (int, error) {
 | |
| 	l := caddy.Log()
 | |
| 
 | |
| 	thisExecPath, err := os.Executable()
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
 | |
| 	}
 | |
| 	thisExecStat, err := os.Stat(thisExecPath)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
 | |
| 	}
 | |
| 	if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
 | |
| 		symSource := thisExecPath
 | |
| 		// we are a symlink; resolve it
 | |
| 		thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
 | |
| 		}
 | |
| 		l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
 | |
| 	}
 | |
| 	l.Info("this executable will be replaced", zap.String("path", thisExecPath))
 | |
| 
 | |
| 	// build the request URL to download this custom build
 | |
| 	qs := url.Values{
 | |
| 		"os":   {runtime.GOOS},
 | |
| 		"arch": {runtime.GOARCH},
 | |
| 	}
 | |
| 	for _, pkgInfo := range pluginPkgs {
 | |
| 		qs.Add("p", pkgInfo.String())
 | |
| 	}
 | |
| 
 | |
| 	// initiate the build
 | |
| 	resp, err := downloadBuild(qs)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	// back up the current binary, in case something goes wrong we can replace it
 | |
| 	backupExecPath := thisExecPath + ".tmp"
 | |
| 	l.Info("build acquired; backing up current executable",
 | |
| 		zap.String("current_path", thisExecPath),
 | |
| 		zap.String("backup_path", backupExecPath))
 | |
| 	err = os.Rename(thisExecPath, backupExecPath)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if err != nil {
 | |
| 			err2 := os.Rename(backupExecPath, thisExecPath)
 | |
| 			if err2 != nil {
 | |
| 				l.Error("restoring original executable failed; will need to be restored manually",
 | |
| 					zap.String("backup_path", backupExecPath),
 | |
| 					zap.String("original_path", thisExecPath),
 | |
| 					zap.Error(err2))
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// download the file; do this in a closure to close reliably before we execute it
 | |
| 	err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 
 | |
| 	l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
 | |
| 
 | |
| 	// use the new binary to print out version and module info
 | |
| 	fmt.Print("\nModule versions:\n\n")
 | |
| 	if err = listModules(thisExecPath); err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy list-modules': %v", err)
 | |
| 	}
 | |
| 	fmt.Println("\nVersion:")
 | |
| 	if err = showVersion(thisExecPath); err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy version': %v", err)
 | |
| 	}
 | |
| 	fmt.Println()
 | |
| 
 | |
| 	// clean up the backup file
 | |
| 	if !fl.Bool("keep-backup") {
 | |
| 		if err = removeCaddyBinary(backupExecPath); err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		l.Info("skipped cleaning up the backup file", zap.String("backup_path", backupExecPath))
 | |
| 	}
 | |
| 
 | |
| 	l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
 | |
| 
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
 | |
| 	bi, ok := debug.ReadBuildInfo()
 | |
| 	if !ok {
 | |
| 		err = fmt.Errorf("no build info")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	for _, modID := range caddy.Modules() {
 | |
| 		modInfo, err := caddy.GetModule(modID)
 | |
| 		if err != nil {
 | |
| 			// that's weird, shouldn't happen
 | |
| 			unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// to get the Caddy plugin's version info, we need to know
 | |
| 		// the package that the Caddy module's value comes from; we
 | |
| 		// can use reflection but we need a non-pointer value (I'm
 | |
| 		// not sure why), and since New() should return a pointer
 | |
| 		// value, we need to dereference it first
 | |
| 		iface := any(modInfo.New())
 | |
| 		if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
 | |
| 			iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
 | |
| 		}
 | |
| 		modPkgPath := reflect.TypeOf(iface).PkgPath()
 | |
| 
 | |
| 		// now we find the Go module that the Caddy module's package
 | |
| 		// belongs to; we assume the Caddy module package path will
 | |
| 		// be prefixed by its Go module path, and we will choose the
 | |
| 		// longest matching prefix in case there are nested modules
 | |
| 		var matched *debug.Module
 | |
| 		for _, dep := range bi.Deps {
 | |
| 			if strings.HasPrefix(modPkgPath, dep.Path) {
 | |
| 				if matched == nil || len(dep.Path) > len(matched.Path) {
 | |
| 					matched = dep
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
 | |
| 
 | |
| 		if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
 | |
| 			standard = append(standard, caddyModGoMod)
 | |
| 		} else {
 | |
| 			nonstandard = append(nonstandard, caddyModGoMod)
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func listModules(path string) error {
 | |
| 	cmd := exec.Command(path, "list-modules", "--versions", "--skip-standard")
 | |
| 	cmd.Stdout = os.Stdout
 | |
| 	cmd.Stderr = os.Stderr
 | |
| 	return cmd.Run()
 | |
| }
 | |
| 
 | |
| func showVersion(path string) error {
 | |
| 	cmd := exec.Command(path, "version")
 | |
| 	cmd.Stdout = os.Stdout
 | |
| 	cmd.Stderr = os.Stderr
 | |
| 	return cmd.Run()
 | |
| }
 | |
| 
 | |
| func downloadBuild(qs url.Values) (*http.Response, error) {
 | |
| 	l := caddy.Log()
 | |
| 	l.Info("requesting build",
 | |
| 		zap.String("os", qs.Get("os")),
 | |
| 		zap.String("arch", qs.Get("arch")),
 | |
| 		zap.Strings("packages", qs["p"]))
 | |
| 	resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("secure request failed: %v", err)
 | |
| 	}
 | |
| 	if resp.StatusCode >= 400 {
 | |
| 		var details struct {
 | |
| 			StatusCode int `json:"status_code"`
 | |
| 			Error      struct {
 | |
| 				Message string `json:"message"`
 | |
| 				ID      string `json:"id"`
 | |
| 			} `json:"error"`
 | |
| 		}
 | |
| 		err2 := json.NewDecoder(resp.Body).Decode(&details)
 | |
| 		if err2 != nil {
 | |
| 			return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
 | |
| 		}
 | |
| 		return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
 | |
| 	}
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) {
 | |
| 	pluginPkgs := make(map[string]pluginPackage)
 | |
| 	for _, mod := range modules {
 | |
| 		if mod.goModule.Replace != nil {
 | |
| 			return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
 | |
| 				mod.goModule.Path, mod.goModule.Replace.Path)
 | |
| 		}
 | |
| 		pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path}
 | |
| 	}
 | |
| 	return pluginPkgs, nil
 | |
| }
 | |
| 
 | |
| func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
 | |
| 	l := caddy.Log()
 | |
| 	destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("unable to open destination file: %v", err)
 | |
| 	}
 | |
| 	defer destFile.Close()
 | |
| 
 | |
| 	l.Info("downloading binary", zap.String("destination", path))
 | |
| 
 | |
| 	_, err = io.Copy(destFile, *body)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("unable to download file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	err = destFile.Sync()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("syncing downloaded file to device: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| const downloadPath = "https://caddyserver.com/api/download"
 | |
| 
 | |
| type pluginPackage struct {
 | |
| 	Version string
 | |
| 	Path    string
 | |
| }
 | |
| 
 | |
| func (p pluginPackage) String() string {
 | |
| 	if p.Version == "" {
 | |
| 		return p.Path
 | |
| 	}
 | |
| 	return p.Path + "@" + p.Version
 | |
| }
 |