Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
cmd: New `add-package` and `remove-package` commands (#4226)
* adding package command * add-package command name * refactoring duplicate code * fixed by review * fixed by review * remove-package command * commands in different files, common utils * fix add, remove, upgrade packages in 1 file * copyright and downloadPath moved * refactor * downloadPath do no export * adding/removing multiple packages * addPackages/removePackages, comments, command-desc * add-package, process case len(args) == 0 Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Oleg committed
Aug 12, 2021 at 02:31 UTC
68c5c71659109b10226f10873f7dc67102b9dc14
3 files changed
+330
-197
cmd/commandfuncs.go
-197
@@ -25,10 +25,8 @@ import (
25
"log"
26
"net"
27
"net/http"
28
- "net/url"
28
"os"
29
"os/exec"
31
- "reflect"
30
"runtime"
31
"runtime/debug"
32
"sort"
@@ -570,151 +568,6 @@ func cmdFmt(fl Flags) (int, error) {
568
return caddy.ExitCodeSuccess, nil
569
}
570
573
-func cmdUpgrade(_ Flags) (int, error) {
574
- l := caddy.Log()
575
-
576
- thisExecPath, err := os.Executable()
577
- if err != nil {
578
- return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
579
- }
580
- thisExecStat, err := os.Stat(thisExecPath)
581
- if err != nil {
582
- return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
583
- }
584
- l.Info("this executable will be replaced", zap.String("path", thisExecPath))
585
-
586
- // get the list of nonstandard plugins
587
- _, nonstandard, _, err := getModules()
588
- if err != nil {
589
- return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
590
- }
591
- pluginPkgs := make(map[string]struct{})
592
- for _, mod := range nonstandard {
593
- if mod.goModule.Replace != nil {
594
- return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
595
- mod.goModule.Path, mod.goModule.Replace.Path)
596
- }
597
- l.Info("found non-standard module",
598
- zap.String("id", mod.caddyModuleID),
599
- zap.String("package", mod.goModule.Path))
600
- pluginPkgs[mod.goModule.Path] = struct{}{}
601
- }
602
-
603
- // build the request URL to download this custom build
604
- qs := url.Values{
605
- "os": {runtime.GOOS},
606
- "arch": {runtime.GOARCH},
607
- }
608
- for pkg := range pluginPkgs {
609
- qs.Add("p", pkg)
610
- }
611
- urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
612
-
613
- // initiate the build
614
- l.Info("requesting build",
615
- zap.String("os", qs.Get("os")),
616
- zap.String("arch", qs.Get("arch")),
617
- zap.Strings("packages", qs["p"]))
618
- resp, err := http.Get(urlStr)
619
- if err != nil {
620
- return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
621
- }
622
- defer resp.Body.Close()
623
- if resp.StatusCode >= 400 {
624
- var details struct {
625
- StatusCode int `json:"status_code"`
626
- Error struct {
627
- Message string `json:"message"`
628
- ID string `json:"id"`
629
- } `json:"error"`
630
- }
631
- err2 := json.NewDecoder(resp.Body).Decode(&details)
632
- if err2 != nil {
633
- return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
634
- }
635
- return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
636
- }
637
-
638
- // back up the current binary, in case something goes wrong we can replace it
639
- backupExecPath := thisExecPath + ".tmp"
640
- l.Info("build acquired; backing up current executable",
641
- zap.String("current_path", thisExecPath),
642
- zap.String("backup_path", backupExecPath))
643
- err = os.Rename(thisExecPath, backupExecPath)
644
- if err != nil {
645
- return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
646
- }
647
- defer func() {
648
- if err != nil {
649
- err2 := os.Rename(backupExecPath, thisExecPath)
650
- if err2 != nil {
651
- l.Error("restoring original executable failed; will need to be restored manually",
652
- zap.String("backup_path", backupExecPath),
653
- zap.String("original_path", thisExecPath),
654
- zap.Error(err2))
655
- }
656
- }
657
- }()
658
-
659
- // download the file; do this in a closure to close reliably before we execute it
660
- writeFile := func() error {
661
- destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, thisExecStat.Mode())
662
- if err != nil {
663
- return fmt.Errorf("unable to open destination file: %v", err)
664
- }
665
- defer destFile.Close()
666
-
667
- l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
668
-
669
- _, err = io.Copy(destFile, resp.Body)
670
- if err != nil {
671
- return fmt.Errorf("unable to download file: %v", err)
672
- }
673
-
674
- err = destFile.Sync()
675
- if err != nil {
676
- return fmt.Errorf("syncing downloaded file to device: %v", err)
677
- }
678
-
679
- return nil
680
- }
681
- err = writeFile()
682
- if err != nil {
683
- return caddy.ExitCodeFailedStartup, err
684
- }
685
-
686
- l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
687
-
688
- // use the new binary to print out version and module info
689
- fmt.Print("\nModule versions:\n\n")
690
- cmd := exec.Command(thisExecPath, "list-modules", "--versions")
691
- cmd.Stdout = os.Stdout
692
- cmd.Stderr = os.Stderr
693
- err = cmd.Run()
694
- if err != nil {
695
- return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
696
- }
697
- fmt.Println("\nVersion:")
698
- cmd = exec.Command(thisExecPath, "version")
699
- cmd.Stdout = os.Stdout
700
- cmd.Stderr = os.Stderr
701
- err = cmd.Run()
702
- if err != nil {
703
- return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
704
- }
705
- fmt.Println()
706
-
707
- // clean up the backup file
708
- err = os.Remove(backupExecPath)
709
- if err != nil {
710
- return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
711
- }
712
-
713
- l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
714
-
715
- return caddy.ExitCodeSuccess, nil
716
-}
717
-
571
func cmdHelp(fl Flags) (int, error) {
572
const fullDocs = `Full documentation is available at:
573
https://caddyserver.com/docs/command-line`
@@ -779,56 +632,6 @@ commands:
632
return caddy.ExitCodeSuccess, nil
633
}
634
782
-func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
783
- bi, ok := debug.ReadBuildInfo()
784
- if !ok {
785
- err = fmt.Errorf("no build info")
786
- return
787
- }
788
-
789
- for _, modID := range caddy.Modules() {
790
- modInfo, err := caddy.GetModule(modID)
791
- if err != nil {
792
- // that's weird, shouldn't happen
793
- unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
794
- continue
795
- }
796
-
797
- // to get the Caddy plugin's version info, we need to know
798
- // the package that the Caddy module's value comes from; we
799
- // can use reflection but we need a non-pointer value (I'm
800
- // not sure why), and since New() should return a pointer
801
- // value, we need to dereference it first
802
- iface := interface{}(modInfo.New())
803
- if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
804
- iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
805
- }
806
- modPkgPath := reflect.TypeOf(iface).PkgPath()
807
-
808
- // now we find the Go module that the Caddy module's package
809
- // belongs to; we assume the Caddy module package path will
810
- // be prefixed by its Go module path, and we will choose the
811
- // longest matching prefix in case there are nested modules
812
- var matched *debug.Module
813
- for _, dep := range bi.Deps {
814
- if strings.HasPrefix(modPkgPath, dep.Path) {
815
- if matched == nil || len(dep.Path) > len(matched.Path) {
816
- matched = dep
817
- }
818
- }
819
- }
820
-
821
- caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
822
-
823
- if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
824
- standard = append(standard, caddyModGoMod)
825
- } else {
826
- nonstandard = append(nonstandard, caddyModGoMod)
827
- }
828
- }
829
- return
830
-}
831
-
635
// apiRequest makes an API request to the endpoint adminAddr with the
636
// given HTTP method and request URI. If body is non-nil, it will be
637
// assumed to be Content-Type application/json.
cmd/commands.go
+24
@@ -291,6 +291,30 @@ Downloads an updated Caddy binary with the same modules/plugins at the
291
latest versions. EXPERIMENTAL: May be changed or removed.`,
292
})
293
294
+ RegisterCommand(Command{
295
+ Name: "add-package",
296
+ Func: cmdAddPackage,
297
+ Usage: "<packages...>",
298
+ Short: "Adds Caddy packages (EXPERIMENTAL)",
299
+ Long: `
300
+Downloads an updated Caddy binary with the specified packages (module/plugin)
301
+added. Retains existing packages. Returns an error if the any of packages are
302
+already included. EXPERIMENTAL: May be changed or removed.
303
+`,
304
+ })
305
+
306
+ RegisterCommand(Command{
307
+ Name: "remove-package",
308
+ Func: cmdRemovePackage,
309
+ Usage: "<packages...>",
310
+ Short: "Removes Caddy packages (EXPERIMENTAL)",
311
+ Long: `
312
+Downloads an updated Caddy binaries without the specified packages (module/plugin).
313
+Returns an error if any of the packages are not included.
314
+EXPERIMENTAL: May be changed or removed.
315
+`,
316
+ })
317
+
318
}
319
320
// RegisterCommand registers the command cmd.
cmd/packagesfuncs.go
new
+306
@@ -0,0 +1,306 @@
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
+ "reflect"
26
+ "runtime"
27
+ "runtime/debug"
28
+ "strings"
29
+
30
+ "github.com/caddyserver/caddy/v2"
31
+ "go.uber.org/zap"
32
+)
33
+
34
+func cmdUpgrade(_ Flags) (int, error) {
35
+ _, nonstandard, _, err := getModules()
36
+ if err != nil {
37
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
38
+ }
39
+ pluginPkgs, err := getPluginPackages(nonstandard)
40
+ if err != nil {
41
+ return caddy.ExitCodeFailedStartup, err
42
+ }
43
+
44
+ return upgradeBuild(pluginPkgs)
45
+}
46
+
47
+func cmdAddPackage(fl Flags) (int, error) {
48
+ if len(fl.Args()) == 0 {
49
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
50
+ }
51
+ _, nonstandard, _, err := getModules()
52
+ if err != nil {
53
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
54
+ }
55
+ pluginPkgs, err := getPluginPackages(nonstandard)
56
+ if err != nil {
57
+ return caddy.ExitCodeFailedStartup, err
58
+ }
59
+
60
+ for _, arg := range fl.Args() {
61
+ if _, ok := pluginPkgs[arg]; ok {
62
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
63
+ }
64
+ pluginPkgs[arg] = struct{}{}
65
+ }
66
+
67
+ return upgradeBuild(pluginPkgs)
68
+}
69
+
70
+func cmdRemovePackage(fl Flags) (int, error) {
71
+ if len(fl.Args()) == 0 {
72
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
73
+ }
74
+ _, nonstandard, _, err := getModules()
75
+ if err != nil {
76
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
77
+ }
78
+ pluginPkgs, err := getPluginPackages(nonstandard)
79
+ if err != nil {
80
+ return caddy.ExitCodeFailedStartup, err
81
+ }
82
+
83
+ for _, arg := range fl.Args() {
84
+ if _, ok := pluginPkgs[arg]; !ok {
85
+ // package does not exist
86
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
87
+ }
88
+ delete(pluginPkgs, arg)
89
+ }
90
+
91
+ return upgradeBuild(pluginPkgs)
92
+}
93
+
94
+func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) {
95
+ l := caddy.Log()
96
+
97
+ thisExecPath, err := os.Executable()
98
+ if err != nil {
99
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
100
+ }
101
+ thisExecStat, err := os.Stat(thisExecPath)
102
+ if err != nil {
103
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
104
+ }
105
+ l.Info("this executable will be replaced", zap.String("path", thisExecPath))
106
+
107
+ // build the request URL to download this custom build
108
+ qs := url.Values{
109
+ "os": {runtime.GOOS},
110
+ "arch": {runtime.GOARCH},
111
+ }
112
+ for pkg := range pluginPkgs {
113
+ qs.Add("p", pkg)
114
+ }
115
+
116
+ // initiate the build
117
+ resp, err := downloadBuild(qs)
118
+ if err != nil {
119
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
120
+ }
121
+ defer resp.Body.Close()
122
+
123
+ // back up the current binary, in case something goes wrong we can replace it
124
+ backupExecPath := thisExecPath + ".tmp"
125
+ l.Info("build acquired; backing up current executable",
126
+ zap.String("current_path", thisExecPath),
127
+ zap.String("backup_path", backupExecPath))
128
+ err = os.Rename(thisExecPath, backupExecPath)
129
+ if err != nil {
130
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
131
+ }
132
+ defer func() {
133
+ if err != nil {
134
+ err2 := os.Rename(backupExecPath, thisExecPath)
135
+ if err2 != nil {
136
+ l.Error("restoring original executable failed; will need to be restored manually",
137
+ zap.String("backup_path", backupExecPath),
138
+ zap.String("original_path", thisExecPath),
139
+ zap.Error(err2))
140
+ }
141
+ }
142
+ }()
143
+
144
+ // download the file; do this in a closure to close reliably before we execute it
145
+ err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
146
+ if err != nil {
147
+ return caddy.ExitCodeFailedStartup, err
148
+ }
149
+
150
+ l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
151
+
152
+ // use the new binary to print out version and module info
153
+ fmt.Print("\nModule versions:\n\n")
154
+ if err = listModules(thisExecPath); err != nil {
155
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
156
+ }
157
+ fmt.Println("\nVersion:")
158
+ if err = showVersion(thisExecPath); err != nil {
159
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
160
+ }
161
+ fmt.Println()
162
+
163
+ // clean up the backup file
164
+ if err = os.Remove(backupExecPath); err != nil {
165
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
166
+ }
167
+ l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
168
+
169
+ return caddy.ExitCodeSuccess, nil
170
+}
171
+
172
+func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
173
+ bi, ok := debug.ReadBuildInfo()
174
+ if !ok {
175
+ err = fmt.Errorf("no build info")
176
+ return
177
+ }
178
+
179
+ for _, modID := range caddy.Modules() {
180
+ modInfo, err := caddy.GetModule(modID)
181
+ if err != nil {
182
+ // that's weird, shouldn't happen
183
+ unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
184
+ continue
185
+ }
186
+
187
+ // to get the Caddy plugin's version info, we need to know
188
+ // the package that the Caddy module's value comes from; we
189
+ // can use reflection but we need a non-pointer value (I'm
190
+ // not sure why), and since New() should return a pointer
191
+ // value, we need to dereference it first
192
+ iface := interface{}(modInfo.New())
193
+ if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
194
+ iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
195
+ }
196
+ modPkgPath := reflect.TypeOf(iface).PkgPath()
197
+
198
+ // now we find the Go module that the Caddy module's package
199
+ // belongs to; we assume the Caddy module package path will
200
+ // be prefixed by its Go module path, and we will choose the
201
+ // longest matching prefix in case there are nested modules
202
+ var matched *debug.Module
203
+ for _, dep := range bi.Deps {
204
+ if strings.HasPrefix(modPkgPath, dep.Path) {
205
+ if matched == nil || len(dep.Path) > len(matched.Path) {
206
+ matched = dep
207
+ }
208
+ }
209
+ }
210
+
211
+ caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
212
+
213
+ if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
214
+ standard = append(standard, caddyModGoMod)
215
+ } else {
216
+ nonstandard = append(nonstandard, caddyModGoMod)
217
+ }
218
+ }
219
+ return
220
+}
221
+
222
+func listModules(path string) error {
223
+ cmd := exec.Command(path, "list-modules", "--versions")
224
+ cmd.Stdout = os.Stdout
225
+ cmd.Stderr = os.Stderr
226
+ err := cmd.Run()
227
+ if err != nil {
228
+ return fmt.Errorf("download succeeded, but unable to execute: %v", err)
229
+ }
230
+ return nil
231
+}
232
+
233
+func showVersion(path string) error {
234
+ cmd := exec.Command(path, "version")
235
+ cmd.Stdout = os.Stdout
236
+ cmd.Stderr = os.Stderr
237
+ err := cmd.Run()
238
+ if err != nil {
239
+ return fmt.Errorf("download succeeded, but unable to execute: %v", err)
240
+ }
241
+ return nil
242
+}
243
+
244
+func downloadBuild(qs url.Values) (*http.Response, error) {
245
+ l := caddy.Log()
246
+ l.Info("requesting build",
247
+ zap.String("os", qs.Get("os")),
248
+ zap.String("arch", qs.Get("arch")),
249
+ zap.Strings("packages", qs["p"]))
250
+ resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
251
+ if err != nil {
252
+ return nil, fmt.Errorf("secure request failed: %v", err)
253
+ }
254
+ if resp.StatusCode >= 400 {
255
+ var details struct {
256
+ StatusCode int `json:"status_code"`
257
+ Error struct {
258
+ Message string `json:"message"`
259
+ ID string `json:"id"`
260
+ } `json:"error"`
261
+ }
262
+ err2 := json.NewDecoder(resp.Body).Decode(&details)
263
+ if err2 != nil {
264
+ return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
265
+ }
266
+ return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
267
+ }
268
+ return resp, nil
269
+}
270
+
271
+func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) {
272
+ pluginPkgs := make(map[string]struct{})
273
+ for _, mod := range modules {
274
+ if mod.goModule.Replace != nil {
275
+ return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
276
+ mod.goModule.Path, mod.goModule.Replace.Path)
277
+ }
278
+ pluginPkgs[mod.goModule.Path] = struct{}{}
279
+ }
280
+ return pluginPkgs, nil
281
+}
282
+
283
+func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
284
+ l := caddy.Log()
285
+ destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
286
+ if err != nil {
287
+ return fmt.Errorf("unable to open destination file: %v", err)
288
+ }
289
+ defer destFile.Close()
290
+
291
+ l.Info("downloading binary", zap.String("destination", path))
292
+
293
+ _, err = io.Copy(destFile, *body)
294
+ if err != nil {
295
+ return fmt.Errorf("unable to download file: %v", err)
296
+ }
297
+
298
+ err = destFile.Sync()
299
+ if err != nil {
300
+ return fmt.Errorf("syncing downloaded file to device: %v", err)
301
+ }
302
+
303
+ return nil
304
+}
305
+
306
+const downloadPath = "https://caddyserver.com/api/download"