@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 230 lines 6.17 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 caddyconfig
16
17 import (
18 "bytes"
19 "encoding/json"
20 "fmt"
21 "io"
22 "mime"
23 "net/http"
24 "strings"
25 "sync"
26
27 "golang.org/x/net/http/httpguts"
28
29 "github.com/caddyserver/caddy/v2"
30 )
31
32 func init() {
33 caddy.RegisterModule(adminLoad{})
34 }
35
36 // adminLoad is a module that provides the /load endpoint
37 // for the Caddy admin API. The only reason it's not baked
38 // into the caddy package directly is because of the import
39 // of the caddyconfig package for its GetAdapter function.
40 // If the caddy package depends on the caddyconfig package,
41 // then the caddyconfig package will not be able to import
42 // the caddy package, and it can more easily cause backward
43 // edges in the dependency tree (i.e. import cycle).
44 // Fortunately, the admin API has first-class support for
45 // adding endpoints from modules.
46 type adminLoad struct{}
47
48 // CaddyModule returns the Caddy module information.
49 func (adminLoad) CaddyModule() caddy.ModuleInfo {
50 return caddy.ModuleInfo{
51 ID: "admin.api.load",
52 New: func() caddy.Module { return new(adminLoad) },
53 }
54 }
55
56 // Routes returns a route for the /load endpoint.
57 func (al adminLoad) Routes() []caddy.AdminRoute {
58 return []caddy.AdminRoute{
59 {
60 Pattern: "/load",
61 Handler: caddy.AdminHandlerFunc(al.handleLoad),
62 },
63 {
64 Pattern: "/adapt",
65 Handler: caddy.AdminHandlerFunc(al.handleAdapt),
66 },
67 }
68 }
69
70 // handleLoad replaces the entire current configuration with
71 // a new one provided in the response body. It supports config
72 // adapters through the use of the Content-Type header. A
73 // config that is identical to the currently-running config
74 // will be a no-op unless Cache-Control: must-revalidate is set.
75 func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
76 if r.Method != http.MethodPost {
77 return caddy.APIError{
78 HTTPStatus: http.StatusMethodNotAllowed,
79 Err: fmt.Errorf("method not allowed"),
80 }
81 }
82
83 buf := bufPool.Get().(*bytes.Buffer)
84 buf.Reset()
85 defer bufPool.Put(buf)
86
87 _, err := io.Copy(buf, r.Body)
88 if err != nil {
89 return caddy.APIError{
90 HTTPStatus: http.StatusBadRequest,
91 Err: fmt.Errorf("reading request body: %v", err),
92 }
93 }
94 body := buf.Bytes()
95
96 var warnings []Warning
97 // if the config is formatted other than Caddy's native
98 // JSON, we need to adapt it before loading it
99 if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
100 result, wns, err := adaptByContentType(ctHeader, body)
101 if err != nil {
102 return caddy.APIError{
103 HTTPStatus: http.StatusBadRequest,
104 Err: err,
105 Warnings: wns,
106 }
107 }
108 warnings = wns
109 body = result
110 }
111
112 forceReload := httpguts.HeaderValuesContainsToken(r.Header["Cache-Control"], "must-revalidate")
113
114 err = caddy.Load(body, forceReload)
115 if err != nil {
116 return caddy.APIError{
117 HTTPStatus: http.StatusBadRequest,
118 Err: fmt.Errorf("loading config: %v", err),
119 Warnings: warnings,
120 }
121 }
122
123 // If this request changed the config, clear the last
124 // config info we have stored, if it is different from
125 // the original source.
126 caddy.ClearLastConfigIfDifferent(
127 r.Header.Get("Caddy-Config-Source-File"),
128 r.Header.Get("Caddy-Config-Source-Adapter"))
129
130 caddy.Log().Named("admin.api").Info("load complete")
131
132 // Send any warnings back even if there were no errors
133 if len(warnings) > 0 {
134 out := struct {
135 Warnings []Warning `json:"warnings"`
136 }{
137 Warnings: warnings,
138 }
139 return json.NewEncoder(w).Encode(out)
140 }
141
142 return nil
143 }
144
145 // handleAdapt adapts the given Caddy config to JSON and responds with the result.
146 func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
147 if r.Method != http.MethodPost {
148 return caddy.APIError{
149 HTTPStatus: http.StatusMethodNotAllowed,
150 Err: fmt.Errorf("method not allowed"),
151 }
152 }
153
154 buf := bufPool.Get().(*bytes.Buffer)
155 buf.Reset()
156 defer bufPool.Put(buf)
157
158 _, err := io.Copy(buf, r.Body)
159 if err != nil {
160 return caddy.APIError{
161 HTTPStatus: http.StatusBadRequest,
162 Err: fmt.Errorf("reading request body: %v", err),
163 }
164 }
165
166 result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
167 if err != nil {
168 return caddy.APIError{
169 HTTPStatus: http.StatusBadRequest,
170 Err: err,
171 }
172 }
173
174 out := struct {
175 Warnings []Warning `json:"warnings,omitempty"`
176 Result json.RawMessage `json:"result"`
177 }{
178 Warnings: warnings,
179 Result: result,
180 }
181
182 w.Header().Set("Content-Type", "application/json")
183 return json.NewEncoder(w).Encode(out)
184 }
185
186 // adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
187 // If contentType is empty or ends with "/json", the input will be returned, as a no-op.
188 func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
189 // assume JSON as the default
190 if contentType == "" {
191 return body, nil, nil
192 }
193
194 ct, _, err := mime.ParseMediaType(contentType)
195 if err != nil {
196 return nil, nil, caddy.APIError{
197 HTTPStatus: http.StatusBadRequest,
198 Err: fmt.Errorf("invalid Content-Type: %v", err),
199 }
200 }
201
202 // if already JSON, no need to adapt
203 if strings.HasSuffix(ct, "/json") {
204 return body, nil, nil
205 }
206
207 // adapter name should be suffix of MIME type
208 _, adapterName, slashFound := strings.Cut(ct, "/")
209 if !slashFound {
210 return nil, nil, fmt.Errorf("malformed Content-Type")
211 }
212
213 cfgAdapter := GetAdapter(adapterName)
214 if cfgAdapter == nil {
215 return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
216 }
217
218 result, warnings, err := cfgAdapter.Adapt(body, nil)
219 if err != nil {
220 return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
221 }
222
223 return result, warnings, nil
224 }
225
226 var bufPool = sync.Pool{
227 New: func() any {
228 return new(bytes.Buffer)
229 },
230 }