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 caddyconfig
16
+
17
+import (
18
+ "bytes"
19
+ "encoding/json"
20
+ "fmt"
21
+ "io"
22
+ "mime"
23
+ "net/http"
24
+ "strings"
25
+ "sync"
26
+
27
+ "github.com/caddyserver/caddy/v2"
28
+)
29
+
30
+func init() {
31
+ caddy.RegisterModule(adminLoad{})
32
+}
33
+
34
+// adminLoad is a module that provides the /load endpoint
35
+// for the Caddy admin API. The only reason it's not baked
36
+// into the caddy package directly is because of the import
37
+// of the caddyconfig package for its GetAdapter function.
38
+// If the caddy package depends on the caddyconfig package,
39
+// then the caddyconfig package will not be able to import
40
+// the caddy package, and it can more easily cause backward
41
+// edges in the dependency tree (i.e. import cycle).
42
+// Fortunately, the admin API has first-class support for
43
+// adding endpoints from modules.
44
+type adminLoad struct{}
45
+
46
+// CaddyModule returns the Caddy module information.
47
+func (adminLoad) CaddyModule() caddy.ModuleInfo {
48
+ return caddy.ModuleInfo{
49
+ ID: "admin.api.load",
50
+ New: func() caddy.Module { return new(adminLoad) },
51
+ }
52
+}
53
+
54
+// Routes returns a route for the /load endpoint.
55
+func (al adminLoad) Routes() []caddy.AdminRoute {
56
+ return []caddy.AdminRoute{
57
+ {
58
+ Pattern: "/load",
59
+ Handler: caddy.AdminHandlerFunc(al.handleLoad),
60
+ },
61
+ }
62
+}
63
+
64
+// handleLoad replaces the entire current configuration with
65
+// a new one provided in the response body. It supports config
66
+// adapters through the use of the Content-Type header. A
67
+// config that is identical to the currently-running config
68
+// will be a no-op unless Cache-Control: must-revalidate is set.
69
+func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
70
+ if r.Method != http.MethodPost {
71
+ return caddy.APIError{
72
+ Code: http.StatusMethodNotAllowed,
73
+ Err: fmt.Errorf("method not allowed"),
74
+ }
75
+ }
76
+
77
+ buf := bufPool.Get().(*bytes.Buffer)
78
+ buf.Reset()
79
+ defer bufPool.Put(buf)
80
+
81
+ _, err := io.Copy(buf, r.Body)
82
+ if err != nil {
83
+ return caddy.APIError{
84
+ Code: http.StatusBadRequest,
85
+ Err: fmt.Errorf("reading request body: %v", err),
86
+ }
87
+ }
88
+ body := buf.Bytes()
89
+
90
+ // if the config is formatted other than Caddy's native
91
+ // JSON, we need to adapt it before loading it
92
+ if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
93
+ ct, _, err := mime.ParseMediaType(ctHeader)
94
+ if err != nil {
95
+ return caddy.APIError{
96
+ Code: http.StatusBadRequest,
97
+ Err: fmt.Errorf("invalid Content-Type: %v", err),
98
+ }
99
+ }
100
+ if !strings.HasSuffix(ct, "/json") {
101
+ slashIdx := strings.Index(ct, "/")
102
+ if slashIdx < 0 {
103
+ return caddy.APIError{
104
+ Code: http.StatusBadRequest,
105
+ Err: fmt.Errorf("malformed Content-Type"),
106
+ }
107
+ }
108
+ adapterName := ct[slashIdx+1:]
109
+ cfgAdapter := GetAdapter(adapterName)
110
+ if cfgAdapter == nil {
111
+ return caddy.APIError{
112
+ Code: http.StatusBadRequest,
113
+ Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
114
+ }
115
+ }
116
+ result, warnings, err := cfgAdapter.Adapt(body, nil)
117
+ if err != nil {
118
+ return caddy.APIError{
119
+ Code: http.StatusBadRequest,
120
+ Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
121
+ }
122
+ }
123
+ if len(warnings) > 0 {
124
+ respBody, err := json.Marshal(warnings)
125
+ if err != nil {
126
+ caddy.Log().Named("admin.api.load").Error(err.Error())
127
+ }
128
+ _, _ = w.Write(respBody)
129
+ }
130
+ body = result
131
+ }
132
+ }
133
+
134
+ forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
135
+
136
+ err = caddy.Load(body, forceReload)
137
+ if err != nil {
138
+ return caddy.APIError{
139
+ Code: http.StatusBadRequest,
140
+ Err: fmt.Errorf("loading config: %v", err),
141
+ }
142
+ }
143
+
144
+ caddy.Log().Named("admin.api").Info("load complete")
145
+
146
+ return nil
147
+}
148
+
149
+var bufPool = sync.Pool{
150
+ New: func() interface{} {
151
+ return new(bytes.Buffer)
152
+ },
153
+}