mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05: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
 | 
						|
}
 |