@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 10, 23:45 UTC

master
go 367 lines 11.3 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddycmd
16
17 import (
18 "encoding/json"
19 "fmt"
20 "io"
21 "net/http"
22 "net/url"
23 "os"
24 "os/exec"
25 "path/filepath"
26 "reflect"
27 "runtime"
28 "runtime/debug"
29 "strings"
30
31 "go.uber.org/zap"
32
33 "github.com/caddyserver/caddy/v2"
34 )
35
36 func cmdUpgrade(fl Flags) (int, error) {
37 _, nonstandard, _, err := getModules()
38 if err != nil {
39 return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
40 }
41 pluginPkgs, err := getPluginPackages(nonstandard)
42 if err != nil {
43 return caddy.ExitCodeFailedStartup, err
44 }
45
46 return upgradeBuild(pluginPkgs, fl)
47 }
48
49 func splitModule(arg string) (module, version string, err error) {
50 const versionSplit = "@"
51
52 // Per https://go.dev/ref/mod#go-mod-file-ident, module paths consist of
53 // ASCII letters, digits, and `-`, `.`, `_`, `~` only, so `@` is never a
54 // valid path character. That makes the split unambiguous: at most one
55 // `@` may appear, and it separates the module path from the version.
56 switch strings.Count(arg, versionSplit) {
57 case 0:
58 module = arg
59 case 1:
60 idx := strings.Index(arg, versionSplit)
61 module, version = arg[:idx], arg[idx+1:]
62 if version == "" {
63 return "", "", fmt.Errorf("version is required after '@'")
64 }
65 default:
66 return "", "", fmt.Errorf("module path must not contain '@'")
67 }
68
69 if module == "" {
70 return "", "", fmt.Errorf("module name is required")
71 }
72
73 return module, version, nil
74 }
75
76 func cmdAddPackage(fl Flags) (int, error) {
77 if len(fl.Args()) == 0 {
78 return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
79 }
80 _, nonstandard, _, err := getModules()
81 if err != nil {
82 return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
83 }
84 pluginPkgs, err := getPluginPackages(nonstandard)
85 if err != nil {
86 return caddy.ExitCodeFailedStartup, err
87 }
88
89 for _, arg := range fl.Args() {
90 module, version, err := splitModule(arg)
91 if err != nil {
92 return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
93 }
94 // only allow a version to be specified if it's different from the existing version
95 if _, ok := pluginPkgs[module]; ok && (version == "" || pluginPkgs[module].Version == version) {
96 return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
97 }
98 pluginPkgs[module] = pluginPackage{Version: version, Path: module}
99 }
100
101 return upgradeBuild(pluginPkgs, fl)
102 }
103
104 func cmdRemovePackage(fl Flags) (int, error) {
105 if len(fl.Args()) == 0 {
106 return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
107 }
108 _, nonstandard, _, err := getModules()
109 if err != nil {
110 return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
111 }
112 pluginPkgs, err := getPluginPackages(nonstandard)
113 if err != nil {
114 return caddy.ExitCodeFailedStartup, err
115 }
116
117 for _, arg := range fl.Args() {
118 module, _, err := splitModule(arg)
119 if err != nil {
120 return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
121 }
122 if _, ok := pluginPkgs[module]; !ok {
123 // package does not exist
124 return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
125 }
126 delete(pluginPkgs, arg)
127 }
128
129 return upgradeBuild(pluginPkgs, fl)
130 }
131
132 func upgradeBuild(pluginPkgs map[string]pluginPackage, fl Flags) (int, error) {
133 l := caddy.Log()
134
135 thisExecPath, err := os.Executable()
136 if err != nil {
137 return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
138 }
139 thisExecStat, err := os.Stat(thisExecPath)
140 if err != nil {
141 return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
142 }
143 if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
144 symSource := thisExecPath
145 // we are a symlink; resolve it
146 thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
147 if err != nil {
148 return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
149 }
150 l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
151 }
152 l.Info("this executable will be replaced", zap.String("path", thisExecPath))
153
154 // build the request URL to download this custom build
155 qs := url.Values{
156 "os": {runtime.GOOS},
157 "arch": {runtime.GOARCH},
158 }
159 for _, pkgInfo := range pluginPkgs {
160 qs.Add("p", pkgInfo.String())
161 }
162
163 // initiate the build
164 resp, err := downloadBuild(qs)
165 if err != nil {
166 return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
167 }
168 defer resp.Body.Close()
169
170 // back up the current binary, in case something goes wrong we can replace it
171 backupExecPath := thisExecPath + ".tmp"
172 l.Info("build acquired; backing up current executable",
173 zap.String("current_path", thisExecPath),
174 zap.String("backup_path", backupExecPath))
175 err = os.Rename(thisExecPath, backupExecPath)
176 if err != nil {
177 return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
178 }
179 defer func() {
180 if err != nil {
181 err2 := os.Rename(backupExecPath, thisExecPath)
182 if err2 != nil {
183 l.Error("restoring original executable failed; will need to be restored manually",
184 zap.String("backup_path", backupExecPath),
185 zap.String("original_path", thisExecPath),
186 zap.Error(err2))
187 }
188 }
189 }()
190
191 // download the file; do this in a closure to close reliably before we execute it
192 err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
193 if err != nil {
194 return caddy.ExitCodeFailedStartup, err
195 }
196
197 l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
198
199 // use the new binary to print out version and module info
200 fmt.Print("\nModule versions:\n\n")
201 if err = listModules(thisExecPath); err != nil {
202 return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy list-modules': %v", err)
203 }
204 fmt.Println("\nVersion:")
205 if err = showVersion(thisExecPath); err != nil {
206 return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute 'caddy version': %v", err)
207 }
208 fmt.Println()
209
210 // clean up the backup file
211 if !fl.Bool("keep-backup") {
212 if err = removeCaddyBinary(backupExecPath); err != nil {
213 return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
214 }
215 } else {
216 l.Info("skipped cleaning up the backup file", zap.String("backup_path", backupExecPath))
217 }
218
219 l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
220
221 return caddy.ExitCodeSuccess, nil
222 }
223
224 func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
225 bi, ok := debug.ReadBuildInfo()
226 if !ok {
227 err = fmt.Errorf("no build info")
228 return standard, nonstandard, unknown, err
229 }
230
231 for _, modID := range caddy.Modules() {
232 modInfo, err := caddy.GetModule(modID)
233 if err != nil {
234 // that's weird, shouldn't happen
235 unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
236 continue
237 }
238
239 // to get the Caddy plugin's version info, we need to know
240 // the package that the Caddy module's value comes from; we
241 // can use reflection but we need a non-pointer value (I'm
242 // not sure why), and since New() should return a pointer
243 // value, we need to dereference it first
244 iface := any(modInfo.New())
245 if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
246 iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
247 }
248 modPkgPath := reflect.TypeOf(iface).PkgPath()
249
250 // now we find the Go module that the Caddy module's package
251 // belongs to; we assume the Caddy module package path will
252 // be prefixed by its Go module path, and we will choose the
253 // longest matching prefix in case there are nested modules
254 var matched *debug.Module
255 for _, dep := range bi.Deps {
256 if moduleContainsPackage(dep.Path, modPkgPath) {
257 if matched == nil || len(dep.Path) > len(matched.Path) {
258 matched = dep
259 }
260 }
261 }
262
263 caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
264
265 if moduleContainsPackage(caddy.ImportPath, modPkgPath) {
266 standard = append(standard, caddyModGoMod)
267 } else {
268 nonstandard = append(nonstandard, caddyModGoMod)
269 }
270 }
271 return standard, nonstandard, unknown, err
272 }
273
274 func moduleContainsPackage(modulePath, packagePath string) bool {
275 return packagePath == modulePath || strings.HasPrefix(packagePath, modulePath+"/")
276 }
277
278 func listModules(path string) error {
279 cmd := exec.Command(path, "list-modules", "--versions", "--skip-standard")
280 cmd.Stdout = os.Stdout
281 cmd.Stderr = os.Stderr
282 return cmd.Run()
283 }
284
285 func showVersion(path string) error {
286 cmd := exec.Command(path, "version")
287 cmd.Stdout = os.Stdout
288 cmd.Stderr = os.Stderr
289 return cmd.Run()
290 }
291
292 func downloadBuild(qs url.Values) (*http.Response, error) {
293 l := caddy.Log()
294 l.Info("requesting build",
295 zap.String("os", qs.Get("os")),
296 zap.String("arch", qs.Get("arch")),
297 zap.Strings("packages", qs["p"]))
298 resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
299 if err != nil {
300 return nil, fmt.Errorf("secure request failed: %v", err)
301 }
302 if resp.StatusCode >= 400 {
303 defer resp.Body.Close()
304 var details struct {
305 StatusCode int `json:"status_code"`
306 Error struct {
307 Message string `json:"message"`
308 ID string `json:"id"`
309 } `json:"error"`
310 }
311 err2 := json.NewDecoder(resp.Body).Decode(&details)
312 if err2 != nil {
313 return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
314 }
315 return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
316 }
317 return resp, nil
318 }
319
320 func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) {
321 pluginPkgs := make(map[string]pluginPackage)
322 for _, mod := range modules {
323 if mod.goModule.Replace != nil {
324 return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
325 mod.goModule.Path, mod.goModule.Replace.Path)
326 }
327 pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path}
328 }
329 return pluginPkgs, nil
330 }
331
332 func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
333 l := caddy.Log()
334 destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
335 if err != nil {
336 return fmt.Errorf("unable to open destination file: %v", err)
337 }
338 defer destFile.Close()
339
340 l.Info("downloading binary", zap.String("destination", path))
341
342 _, err = io.Copy(destFile, *body)
343 if err != nil {
344 return fmt.Errorf("unable to download file: %v", err)
345 }
346
347 err = destFile.Sync()
348 if err != nil {
349 return fmt.Errorf("syncing downloaded file to device: %v", err)
350 }
351
352 return nil
353 }
354
355 const downloadPath = "https://caddyserver.com/api/download"
356
357 type pluginPackage struct {
358 Version string
359 Path string
360 }
361
362 func (p pluginPackage) String() string {
363 if p.Version == "" {
364 return p.Path
365 }
366 return p.Path + "@" + p.Version
367 }