@marcel91200 / caddy Mirror · read-only / commits / 35f70c98
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 14, 05:15 UTC

core: Major refactor of admin endpoint and config handling

Fixed several bugs and made other improvements. All config changes are now mediated by the global config state manager. It used to be that initial configs given at startup weren't tracked, so you could start caddy with --config caddy.json and then do a GET /config/ and it would return null. That is fixed, along with several other general flow/API enhancements, with more to come.

Matthew Holt committed Nov 4, 2019 at 12:05 UTC 35f70c98fa1ea13882ee4f0406cd17f5545d0100
8 files changed +961 -682
admin.go
+642 -157
@@ -18,157 +18,202 @@ import (
18 "bytes"
19 "context"
20 "encoding/json"
21 - "errors"
21 + "expvar"
22 "fmt"
23 "io"
24 - "io/ioutil"
25 - "log"
24 "mime"
27 - "net"
25 "net/http"
26 "net/http/pprof"
27 + "net/url"
28 "os"
29 + "path"
30 + "strconv"
31 "strings"
32 "sync"
33 "time"
34
35 "github.com/caddyserver/caddy/v2/caddyconfig"
36 - "github.com/mholt/certmagic"
37 - "github.com/rs/cors"
36 "go.uber.org/zap"
37 )
38
41 -var (
42 - cfgEndptSrv *http.Server
43 - cfgEndptSrvMu sync.Mutex
44 -)
45 -
46 -var ErrAdminInterfaceNotConfigured = errors.New("no admin configuration has been set")
39 +// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
40
41 // AdminConfig configures the admin endpoint.
42 type AdminConfig struct {
50 - Listen string `json:"listen,omitempty"`
43 + Disabled bool `json:"disabled,omitempty"`
44 + Listen string `json:"listen,omitempty"`
45 + EnforceOrigin bool `json:"enforce_origin,omitempty"`
46 + Origins []string `json:"origins,omitempty"`
47 }
48
53 -// DefaultAdminConfig is the default configuration
54 -// for the administration endpoint.
55 -var DefaultAdminConfig = &AdminConfig{
56 - Listen: DefaultAdminListen,
49 +// listenAddr extracts a singular listen address from ac.Listen,
50 +// returning the network and the address of the listener.
51 +func (admin AdminConfig) listenAddr() (netw string, addr string, err error) {
52 + var listenAddrs []string
53 + input := admin.Listen
54 + if input == "" {
55 + input = DefaultAdminListen
56 + }
57 + netw, listenAddrs, err = ParseNetworkAddress(input)
58 + if err != nil {
59 + err = fmt.Errorf("parsing admin listener address: %v", err)
60 + return
61 + }
62 + if len(listenAddrs) != 1 {
63 + err = fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddrs)
64 + return
65 + }
66 + addr = listenAddrs[0]
67 + return
68 }
69
59 -// TODO: holy smokes, the admin endpoint might not have to live in caddy's core.
60 -
61 -// StartAdmin starts Caddy's administration endpoint,
62 -// bootstrapping it with an optional configuration
63 -// in the format of JSON bytes. It opens a listener
64 -// resource. When no longer needed, StopAdmin should
65 -// be called.
66 -// If no configuration is given, a default listener is
67 -// started. If a configuration is given that does NOT
68 -// specifically configure the admin interface,
69 -// `ErrAdminInterfaceNotConfigured` is returned and no
70 -// listener is initialized.
71 -func StartAdmin(initialConfigJSON []byte) error {
72 - cfgEndptSrvMu.Lock()
73 - defer cfgEndptSrvMu.Unlock()
74 -
75 - if cfgEndptSrv != nil {
76 - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
77 - defer cancel()
78 - err := cfgEndptSrv.Shutdown(ctx)
79 - if err != nil {
80 - return fmt.Errorf("shutting down old admin endpoint: %v", err)
81 - }
70 +// newAdminHandler reads admin's config and returns an http.Handler suitable
71 +// for use in an admin endpoint server, which will be listening on listenAddr.
72 +func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
73 + muxWrap := adminHandler{
74 + enforceOrigin: admin.EnforceOrigin,
75 + allowedOrigins: admin.allowedOrigins(listenAddr),
76 + mux: http.NewServeMux(),
77 }
78
84 - adminConfig := DefaultAdminConfig
85 - if len(initialConfigJSON) > 0 {
86 - var config *Config
87 - err := json.Unmarshal(initialConfigJSON, &config)
88 - if err != nil {
89 - return fmt.Errorf("unmarshaling bootstrap config: %v", err)
90 - }
91 - if config != nil {
92 - if config.Admin == nil {
93 - return ErrAdminInterfaceNotConfigured
94 - }
95 - adminConfig = config.Admin
79 + // addRoute just calls muxWrap.mux.Handle after
80 + // wrapping the handler with error handling
81 + addRoute := func(pattern string, h AdminHandler) {
82 + wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83 + err := h.ServeHTTP(w, r)
84 + muxWrap.handleError(w, r, err)
85 + })
86 + muxWrap.mux.Handle(pattern, wrapper)
87 + }
88 +
89 + // register standard config control endpoints
90 + addRoute("/load", AdminHandlerFunc(handleLoad))
91 + addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
92 + addRoute("/id/", AdminHandlerFunc(handleConfigID))
93 + addRoute("/unload", AdminHandlerFunc(handleUnload))
94 + addRoute("/stop", AdminHandlerFunc(handleStop))
95 +
96 + // register debugging endpoints
97 + muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
98 + muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
99 + muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
100 + muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
101 + muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
102 + muxWrap.mux.Handle("/debug/vars", expvar.Handler())
103 +
104 + // register third-party module endpoints
105 + for _, m := range GetModules("admin.api") {
106 + router := m.New().(AdminRouter)
107 + for _, route := range router.Routes() {
108 + addRoute(route.Pattern, route.Handler)
109 }
110 }
111
99 - // extract a singular listener address
100 - netw, listenAddrs, err := ParseNetworkAddress(adminConfig.Listen)
101 - if err != nil {
102 - return fmt.Errorf("parsing admin listener address: %v", err)
112 + return muxWrap
113 +}
114 +
115 +// allowedOrigins returns a list of origins that are allowed.
116 +// If admin.Origins is nil (null), the provided listen address
117 +// will be used as the default origin. If admin.Origins is
118 +// empty, no origins will be allowed, effectively bricking the
119 +// endpoint, but whatever.
120 +func (admin AdminConfig) allowedOrigins(listen string) []string {
121 + uniqueOrigins := make(map[string]struct{})
122 + for _, o := range admin.Origins {
123 + uniqueOrigins[o] = struct{}{}
124 }
104 - if len(listenAddrs) != 1 {
105 - return fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddrs)
125 + if admin.Origins == nil {
126 + uniqueOrigins[listen] = struct{}{}
127 }
107 - ln, err := net.Listen(netw, listenAddrs[0])
108 - if err != nil {
109 - return err
128 + var allowed []string
129 + for origin := range uniqueOrigins {
130 + allowed = append(allowed, origin)
131 }
132 + return allowed
133 +}
134
112 - mux := http.NewServeMux()
113 - mux.HandleFunc("/load", handleLoadConfig)
114 - mux.HandleFunc("/stop", handleStop)
135 +// replaceAdmin replaces the running admin server according
136 +// to the relevant configuration in cfg. If no configuration
137 +// for the admin endpoint exists in cfg, a default one is
138 +// used, so that there is always an admin server (unless it
139 +// is explicitly configured to be disabled).
140 +func replaceAdmin(cfg *Config) error {
141 + // always be sure to close down the old admin endpoint
142 + // as gracefully as possible, even if the new one is
143 + // disabled -- careful to use reference to the current
144 + // (old) admin endpoint since it will be different
145 + // when the function returns
146 + oldAdminServer := adminServer
147 + defer func() {
148 + // do the shutdown asynchronously so that any
149 + // current API request gets a response; this
150 + // goroutine may last a few seconds
151 + if oldAdminServer != nil {
152 + go func(oldAdminServer *http.Server) {
153 + err := stopAdminServer(oldAdminServer)
154 + if err != nil {
155 + Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
156 + }
157 + }(oldAdminServer)
158 + }
159 + }()
160
116 - ///// BEGIN PPROF STUFF (TODO: Temporary) /////
117 - mux.HandleFunc("/debug/pprof/", pprof.Index)
118 - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
119 - mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
120 - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
121 - mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
122 - ///// END PPROF STUFF //////
161 + // always get a valid admin config
162 + adminConfig := DefaultAdminConfig
163 + if cfg != nil && cfg.Admin != nil {
164 + adminConfig = cfg.Admin
165 + }
166
124 - for _, m := range GetModules("admin.routers") {
125 - adminrtr := m.New().(AdminRouter)
126 - for _, route := range adminrtr.Routes() {
127 - mux.Handle(route.Pattern, route)
128 - }
167 + // if new admin endpoint is to be disabled, we're done
168 + if adminConfig.Disabled {
169 + Log().Named("admin").Warn("admin endpoint disabled")
170 + return nil
171 }
172
131 - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132 - // TODO: improve/organize this logging
133 - Log().Named("admin.request").Info("",
134 - zap.String("method", r.Method),
135 - zap.String("uri", r.RequestURI),
136 - zap.String("remote", r.RemoteAddr),
137 - )
138 - cors.Default().Handler(mux).ServeHTTP(w, r)
139 - })
173 + // extract a singular listener address
174 + netw, addr, err := adminConfig.listenAddr()
175 + if err != nil {
176 + return err
177 + }
178
141 - cfgEndptSrv = &http.Server{
179 + handler := adminConfig.newAdminHandler(addr)
180 +
181 + ln, err := Listen(netw, addr)
182 + if err != nil {
183 + return err
184 + }
185 +
186 + adminServer = &http.Server{
187 Handler: handler,
143 - ReadTimeout: 5 * time.Second,
188 + ReadTimeout: 10 * time.Second,
189 ReadHeaderTimeout: 5 * time.Second,
145 - IdleTimeout: 5 * time.Second,
190 + IdleTimeout: 60 * time.Second,
191 MaxHeaderBytes: 1024 * 64,
192 }
193
149 - go cfgEndptSrv.Serve(ln)
194 + go adminServer.Serve(ln)
195
151 - Log().Named("admin").Info("Caddy 2 admin endpoint started.", zap.String("listenAddress", adminConfig.Listen))
196 + Log().Named("admin").Info(
197 + "admin endpoint started",
198 + zap.String("address", addr),
199 + zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
200 + zap.Strings("origins", handler.allowedOrigins),
201 + )
202
203 return nil
204 }
205
156 -// StopAdmin stops the API endpoint.
157 -func StopAdmin() error {
158 - cfgEndptSrvMu.Lock()
159 - defer cfgEndptSrvMu.Unlock()
160 -
161 - if cfgEndptSrv == nil {
162 - return fmt.Errorf("no server")
206 +func stopAdminServer(srv *http.Server) error {
207 + if srv == nil {
208 + return fmt.Errorf("no admin server")
209 }
164 -
165 - err := cfgEndptSrv.Shutdown(context.Background()) // TODO
210 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
211 + defer cancel()
212 + err := srv.Shutdown(ctx)
213 if err != nil {
167 - return fmt.Errorf("shutting down server: %v", err)
214 + return fmt.Errorf("shutting down admin server: %v", err)
215 }
169 -
170 - cfgEndptSrv = nil
171 -
216 + Log().Named("admin").Info("stopped previous server")
217 return nil
218 }
219
@@ -179,117 +224,557 @@ type AdminRouter interface {
224
225 // AdminRoute represents a route for the admin endpoint.
226 type AdminRoute struct {
182 - http.Handler
227 Pattern string
228 + Handler AdminHandler
229 }
230
186 -func handleLoadConfig(w http.ResponseWriter, r *http.Request) {
187 - if r.Method != http.MethodPost {
188 - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
231 +type adminHandler struct {
232 + enforceOrigin bool
233 + allowedOrigins []string
234 + mux *http.ServeMux
235 +}
236 +
237 +// ServeHTTP is the external entry point for API requests.
238 +// It will only be called once per request.
239 +func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
240 + Log().Named("admin.api").Info("received request",
241 + zap.String("method", r.Method),
242 + zap.String("uri", r.RequestURI),
243 + zap.String("remote_addr", r.RemoteAddr),
244 + zap.Reflect("headers", r.Header),
245 + )
246 + h.serveHTTP(w, r)
247 +}
248 +
249 +// serveHTTP is the internal entry point for API requests. It may
250 +// be called more than once per request, for example if a request
251 +// is rewritten (i.e. internal redirect).
252 +func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
253 + if h.enforceOrigin {
254 + // DNS rebinding mitigation
255 + err := h.checkHost(r)
256 + if err != nil {
257 + h.handleError(w, r, err)
258 + return
259 + }
260 +
261 + // cross-site mitigation
262 + origin, err := h.checkOrigin(r)
263 + if err != nil {
264 + h.handleError(w, r, err)
265 + return
266 + }
267 +
268 + w.Header().Set("Access-Control-Allow-Origin", origin)
269 + w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
270 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
271 + w.Header().Set("Access-Control-Allow-Credentials", "true")
272 + }
273 +
274 + // TODO: authentication & authorization, if configured
275 +
276 + h.mux.ServeHTTP(w, r)
277 +}
278 +
279 +func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
280 + if err == nil {
281 return
282 }
283 + if err == ErrInternalRedir {
284 + h.serveHTTP(w, r)
285 + return
286 + }
287 +
288 + apiErr, ok := err.(APIError)
289 + if !ok {
290 + apiErr = APIError{
291 + Code: http.StatusInternalServerError,
292 + Err: err,
293 + }
294 + }
295 + if apiErr.Code == 0 {
296 + apiErr.Code = http.StatusInternalServerError
297 + }
298 + if apiErr.Message == "" && apiErr.Err != nil {
299 + apiErr.Message = apiErr.Err.Error()
300 + }
301 +
302 + Log().Named("admin.api").Error("request error",
303 + zap.Error(err),
304 + zap.Int("status_code", apiErr.Code),
305 + )
306 +
307 + w.Header().Set("Content-Type", "application/json")
308 + w.WriteHeader(apiErr.Code)
309 + json.NewEncoder(w).Encode(apiErr)
310 +}
311 +
312 +// checkHost returns a handler that wraps next such that
313 +// it will only be called if the request's Host header matches
314 +// a trustworthy/expected value. This helps to mitigate DNS
315 +// rebinding attacks.
316 +func (h adminHandler) checkHost(r *http.Request) error {
317 + var allowed bool
318 + for _, allowedHost := range h.allowedOrigins {
319 + if r.Host == allowedHost {
320 + allowed = true
321 + break
322 + }
323 + }
324 + if !allowed {
325 + return APIError{
326 + Code: http.StatusForbidden,
327 + Err: fmt.Errorf("host not allowed: %s", r.Host),
328 + }
329 + }
330 + return nil
331 +}
332 +
333 +// checkOrigin ensures that the Origin header, if
334 +// set, matches the intended target; prevents arbitrary
335 +// sites from issuing requests to our listener. It
336 +// returns the origin that was obtained from r.
337 +func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
338 + origin := h.getOriginHost(r)
339 + if origin == "" {
340 + return origin, APIError{
341 + Code: http.StatusForbidden,
342 + Err: fmt.Errorf("missing required Origin header"),
343 + }
344 + }
345 + if !h.originAllowed(origin) {
346 + return origin, APIError{
347 + Code: http.StatusForbidden,
348 + Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
349 + }
350 + }
351 + return origin, nil
352 +}
353 +
354 +func (h adminHandler) getOriginHost(r *http.Request) string {
355 + origin := r.Header.Get("Origin")
356 + if origin == "" {
357 + origin = r.Header.Get("Referer")
358 + }
359 + originURL, err := url.Parse(origin)
360 + if err == nil && originURL.Host != "" {
361 + origin = originURL.Host
362 + }
363 + return origin
364 +}
365 +
366 +func (h adminHandler) originAllowed(origin string) bool {
367 + for _, allowedOrigin := range h.allowedOrigins {
368 + originCopy := origin
369 + if !strings.Contains(allowedOrigin, "://") {
370 + // no scheme specified, so allow both
371 + originCopy = strings.TrimPrefix(originCopy, "http://")
372 + originCopy = strings.TrimPrefix(originCopy, "https://")
373 + }
374 + if originCopy == allowedOrigin {
375 + return true
376 + }
377 + }
378 + return false
379 +}
380 +
381 +func handleLoad(w http.ResponseWriter, r *http.Request) error {
382 + if r.Method != http.MethodPost {
383 + return APIError{
384 + Code: http.StatusMethodNotAllowed,
385 + Err: fmt.Errorf("method not allowed"),
386 + }
387 + }
388 +
389 + buf := bufPool.Get().(*bytes.Buffer)
390 + buf.Reset()
391 + defer bufPool.Put(buf)
392 +
393 + _, err := io.Copy(buf, r.Body)
394 + if err != nil {
395 + return APIError{
396 + Code: http.StatusBadRequest,
397 + Err: fmt.Errorf("reading request body: %v", err),
398 + }
399 + }
400 + body := buf.Bytes()
401
402 // if the config is formatted other than Caddy's native
403 // JSON, we need to adapt it before loading it
404 if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
405 ct, _, err := mime.ParseMediaType(ctHeader)
406 if err != nil {
197 - http.Error(w, "Invalid Content-Type: "+err.Error(), http.StatusBadRequest)
198 - return
407 + return APIError{
408 + Code: http.StatusBadRequest,
409 + Err: fmt.Errorf("invalid Content-Type: %v", err),
410 + }
411 }
412 if !strings.HasSuffix(ct, "/json") {
413 slashIdx := strings.Index(ct, "/")
414 if slashIdx < 0 {
203 - http.Error(w, "Malformed Content-Type", http.StatusBadRequest)
204 - return
415 + return APIError{
416 + Code: http.StatusBadRequest,
417 + Err: fmt.Errorf("malformed Content-Type"),
418 + }
419 }
420 adapterName := ct[slashIdx+1:]
421 cfgAdapter := caddyconfig.GetAdapter(adapterName)
422 if cfgAdapter == nil {
209 - http.Error(w, "Unrecognized config adapter: "+adapterName, http.StatusBadRequest)
210 - return
211 - }
212 - body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, 1024*1024))
213 - if err != nil {
214 - http.Error(w, "Error reading request body: "+err.Error(), http.StatusBadRequest)
215 - return
423 + return APIError{
424 + Code: http.StatusBadRequest,
425 + Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
426 + }
427 }
428 result, warnings, err := cfgAdapter.Adapt(body, nil)
429 if err != nil {
219 - log.Printf("[ADMIN][ERROR] adapting config from %s: %v", adapterName, err)
220 - http.Error(w, fmt.Sprintf("Adapting config from %s: %v", adapterName, err), http.StatusBadRequest)
221 - return
430 + return APIError{
431 + Code: http.StatusBadRequest,
432 + Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
433 + }
434 }
435 if len(warnings) > 0 {
436 respBody, err := json.Marshal(warnings)
437 if err != nil {
226 - log.Printf("[ADMIN][ERROR] marshaling warnings: %v", err)
438 + Log().Named("admin.api.load").Error(err.Error())
439 }
440 w.Write(respBody)
441 }
230 - // replace original request body with adapted JSON
231 - r.Body.Close()
232 - r.Body = ioutil.NopCloser(bytes.NewReader(result))
442 + body = result
443 + }
444 + }
445 +
446 + forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
447 +
448 + err = Load(body, forceReload)
449 + if err != nil {
450 + return APIError{
451 + Code: http.StatusBadRequest,
452 + Err: fmt.Errorf("loading config: %v", err),
453 + }
454 + }
455 +
456 + Log().Named("admin.api").Info("load complete")
457 +
458 + return nil
459 +}
460 +
461 +func handleConfig(w http.ResponseWriter, r *http.Request) error {
462 + switch r.Method {
463 + case http.MethodGet:
464 + w.Header().Set("Content-Type", "application/json")
465 +
466 + err := readConfig(r.URL.Path, w)
467 + if err != nil {
468 + return APIError{Code: http.StatusBadRequest, Err: err}
469 + }
470 +
471 + return nil
472 +
473 + case http.MethodPost,
474 + http.MethodPut,
475 + http.MethodPatch,
476 + http.MethodDelete:
477 +
478 + // DELETE does not use a body, but the others do
479 + var body []byte
480 + if r.Method != http.MethodDelete {
481 + if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
482 + return APIError{
483 + Code: http.StatusBadRequest,
484 + Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
485 + }
486 + }
487 +
488 + buf := bufPool.Get().(*bytes.Buffer)
489 + buf.Reset()
490 + defer bufPool.Put(buf)
491 +
492 + _, err := io.Copy(buf, r.Body)
493 + if err != nil {
494 + return APIError{
495 + Code: http.StatusBadRequest,
496 + Err: fmt.Errorf("reading request body: %v", err),
497 + }
498 + }
499 + body = buf.Bytes()
500 }
501 +
502 + forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
503 +
504 + err := changeConfig(r.Method, r.URL.Path, body, forceReload)
505 + if err != nil {
506 + return err
507 + }
508 +
509 + default:
510 + return APIError{
511 + Code: http.StatusMethodNotAllowed,
512 + Err: fmt.Errorf("method %s not allowed", r.Method),
513 + }
514 + }
515 +
516 + return nil
517 +}
518 +
519 +func handleConfigID(w http.ResponseWriter, r *http.Request) error {
520 + idPath := r.URL.Path
521 +
522 + parts := strings.Split(idPath, "/")
523 + if len(parts) < 3 || parts[2] == "" {
524 + return fmt.Errorf("request path is missing object ID")
525 + }
526 + if parts[0] != "" || parts[1] != "id" {
527 + return fmt.Errorf("malformed object path")
528 + }
529 + id := parts[2]
530 +
531 + // map the ID to the expanded path
532 + currentCfgMu.RLock()
533 + expanded, ok := rawCfgIndex[id]
534 + defer currentCfgMu.RUnlock()
535 + if !ok {
536 + return fmt.Errorf("unknown object ID '%s'", id)
537 }
538
236 - // pass this off to the /config/ endpoint
237 - r.URL.Path = "/" + rawConfigKey + "/"
238 - handleConfig(w, r)
539 + // piece the full URL path back together
540 + parts = append([]string{expanded}, parts[3:]...)
541 + r.URL.Path = path.Join(parts...)
542 +
543 + return ErrInternalRedir
544 }
545
241 -func handleStop(w http.ResponseWriter, r *http.Request) {
546 +func handleUnload(w http.ResponseWriter, r *http.Request) error {
547 if r.Method != http.MethodPost {
243 - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
244 - return
548 + return APIError{
549 + Code: http.StatusMethodNotAllowed,
550 + Err: fmt.Errorf("method not allowed"),
551 + }
552 + }
553 + currentCfgMu.RLock()
554 + hasCfg := currentCfg != nil
555 + currentCfgMu.RUnlock()
556 + if !hasCfg {
557 + Log().Named("admin.api").Info("nothing to unload")
558 + return nil
559 }
246 - log.Println("[ADMIN] Initiating shutdown")
560 + Log().Named("admin.api").Info("unloading")
561 if err := stopAndCleanup(); err != nil {
248 - log.Printf("[ADMIN][ERROR] stopping: %v \n", err)
562 + Log().Named("admin.api").Error("error unloading", zap.Error(err))
563 + } else {
564 + Log().Named("admin.api").Info("unloading completed")
565 }
250 - log.Println("[ADMIN] Exiting")
251 - os.Exit(0)
566 + return nil
567 }
568
254 -func stopAndCleanup() error {
255 - if err := Stop(); err != nil {
256 - return err
569 +func handleStop(w http.ResponseWriter, r *http.Request) error {
570 + defer func() {
571 + Log().Named("admin.api").Info("stopping now, bye!! 👋")
572 + os.Exit(0)
573 + }()
574 + err := handleUnload(w, r)
575 + if err != nil {
576 + Log().Named("admin.api").Error("unload error", zap.Error(err))
577 }
258 - certmagic.CleanUpOwnLocks()
578 return nil
579 }
580
262 -// Load loads and starts a configuration.
263 -func Load(r io.Reader) error {
264 - buf := bufPool.Get().(*bytes.Buffer)
265 - buf.Reset()
266 - defer bufPool.Put(buf)
581 +// unsyncedConfigAccess traverses into the current config and performs
582 +// the operation at path according to method, using body and out as
583 +// needed. This is a low-level, unsynchronized function; most callers
584 +// will want to use changeConfig or readConfig instead. This requires a
585 +// read or write lock on currentCfgMu, depending on method (GET needs
586 +// only a read lock; all others need a write lock).
587 +func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
588 + var err error
589 + var val interface{}
590 +
591 + // if there is a request body, decode it into the
592 + // variable that will be set in the config according
593 + // to method and path
594 + if len(body) > 0 {
595 + err = json.Unmarshal(body, &val)
596 + if err != nil {
597 + return fmt.Errorf("decoding request body: %v", err)
598 + }
599 + }
600
268 - _, err := io.Copy(buf, io.LimitReader(r, 1024*1024))
269 - if err != nil {
270 - return err
601 + enc := json.NewEncoder(out)
602 +
603 + cleanPath := strings.Trim(path, "/")
604 + if cleanPath == "" {
605 + return fmt.Errorf("no traversable path")
606 }
607
273 - var cfg *Config
274 - err = json.Unmarshal(buf.Bytes(), &cfg)
275 - if err != nil {
276 - return fmt.Errorf("decoding config: %v", err)
608 + parts := strings.Split(cleanPath, "/")
609 + if len(parts) == 0 {
610 + return fmt.Errorf("path missing")
611 }
612
279 - err = Run(cfg)
280 - if err != nil {
281 - return fmt.Errorf("running: %v", err)
613 + var ptr interface{} = rawCfg
614 +
615 +traverseLoop:
616 + for i, part := range parts {
617 + switch v := ptr.(type) {
618 + case map[string]interface{}:
619 + // if the next part enters a slice, and the slice is our destination,
620 + // handle it specially (because appending to the slice copies the slice
621 + // header, which does not replace the original one like we want)
622 + if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
623 + var idx int
624 + if method != http.MethodPost {
625 + idxStr := parts[len(parts)-1]
626 + idx, err = strconv.Atoi(idxStr)
627 + if err != nil {
628 + return fmt.Errorf("[%s] invalid array index '%s': %v",
629 + path, idxStr, err)
630 + }
631 + if idx < 0 || idx >= len(arr) {
632 + return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
633 + }
634 + }
635 +
636 + switch method {
637 + case http.MethodGet:
638 + err = enc.Encode(arr[idx])
639 + if err != nil {
640 + return fmt.Errorf("encoding config: %v", err)
641 + }
642 + case http.MethodPost:
643 + v[part] = append(arr, val)
644 + case http.MethodPut:
645 + // avoid creation of new slice and a second copy (see
646 + // https://github.com/golang/go/wiki/SliceTricks#insert)
647 + arr = append(arr, nil)
648 + copy(arr[idx+1:], arr[idx:])
649 + arr[idx] = val
650 + v[part] = arr
651 + case http.MethodPatch:
652 + arr[idx] = val
653 + case http.MethodDelete:
654 + v[part] = append(arr[:idx], arr[idx+1:]...)
655 + default:
656 + return fmt.Errorf("unrecognized method %s", method)
657 + }
658 + break traverseLoop
659 + }
660 +
661 + if i == len(parts)-1 {
662 + switch method {
663 + case http.MethodGet:
664 + err = enc.Encode(v[part])
665 + if err != nil {
666 + return fmt.Errorf("encoding config: %v", err)
667 + }
668 + case http.MethodPost:
669 + if arr, ok := v[part].([]interface{}); ok {
670 + // if the part is an existing list, POST appends to it
671 + // TODO: Do we ever reach this point, since we handle arrays
672 + // separately above?
673 + v[part] = append(arr, val)
674 + } else {
675 + // otherwise, it simply sets the value
676 + v[part] = val
677 + }
678 + case http.MethodPut:
679 + if _, ok := v[part]; ok {
680 + return fmt.Errorf("[%s] key already exists: %s", path, part)
681 + }
682 + v[part] = val
683 + case http.MethodPatch:
684 + if _, ok := v[part]; !ok {
685 + return fmt.Errorf("[%s] key does not exist: %s", path, part)
686 + }
687 + v[part] = val
688 + case http.MethodDelete:
689 + delete(v, part)
690 + default:
691 + return fmt.Errorf("unrecognized method %s", method)
692 + }
693 + } else {
694 + ptr = v[part]
695 + }
696 +
697 + case []interface{}:
698 + partInt, err := strconv.Atoi(part)
699 + if err != nil {
700 + return fmt.Errorf("[/%s] invalid array index '%s': %v",
701 + strings.Join(parts[:i+1], "/"), part, err)
702 + }
703 + if partInt < 0 || partInt >= len(v) {
704 + return fmt.Errorf("[/%s] array index out of bounds: %s",
705 + strings.Join(parts[:i+1], "/"), part)
706 + }
707 + ptr = v[partInt]
708 +
709 + default:
710 + return fmt.Errorf("invalid path: %s", parts[:i+1])
711 + }
712 }
713
714 return nil
715 }
716
287 -// DefaultAdminListen is the address for the admin
288 -// listener, if none is specified at startup.
289 -var DefaultAdminListen = "localhost:2019"
717 +// AdminHandler is like http.Handler except ServeHTTP may return an error.
718 +//
719 +// If any handler encounters an error, it should be returned for proper
720 +// handling.
721 +type AdminHandler interface {
722 + ServeHTTP(http.ResponseWriter, *http.Request) error
723 +}
724 +
725 +// AdminHandlerFunc is a convenience type like http.HandlerFunc.
726 +type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
727 +
728 +// ServeHTTP implements the Handler interface.
729 +func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
730 + return f(w, r)
731 +}
732 +
733 +// APIError is a structured error that every API
734 +// handler should return for consistency in logging
735 +// and client responses. If Message is unset, then
736 +// Err.Error() will be serialized in its place.
737 +type APIError struct {
738 + Code int `json:"-"`
739 + Err error `json:"-"`
740 + Message string `json:"error"`
741 +}
742 +
743 +func (e APIError) Error() string {
744 + if e.Err != nil {
745 + return e.Err.Error()
746 + }
747 + return e.Message
748 +}
749 +
750 +var (
751 + // DefaultAdminListen is the address for the admin
752 + // listener, if none is specified at startup.
753 + DefaultAdminListen = "localhost:2019"
754 +
755 + // ErrInternalRedir indicates an internal redirect
756 + // and is useful when admin API handlers rewrite
757 + // the request; in that case, authentication and
758 + // authorization needs to happen again for the
759 + // rewritten request.
760 + ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
761 +
762 + // DefaultAdminConfig is the default configuration
763 + // for the administration endpoint.
764 + DefaultAdminConfig = &AdminConfig{
765 + Listen: DefaultAdminListen,
766 + }
767 +)
768 +
769 +const (
770 + rawConfigKey = "config"
771 + idKey = "@id"
772 +)
773
774 var bufPool = sync.Pool{
775 New: func() interface{} {
776 return new(bytes.Buffer)
777 },
778 }
779 +
780 +var adminServer *http.Server
admin_test.go
+86 -4
@@ -15,14 +15,96 @@
15 package caddy
16
17 import (
18 - "strings"
18 + "encoding/json"
19 + "reflect"
20 "testing"
21 )
22
23 +func TestUnsyncedConfigAccess(t *testing.T) {
24 + // each test is performed in sequence, so
25 + // each change builds on the previous ones;
26 + // the config is not reset between tests
27 + for i, tc := range []struct {
28 + method string
29 + path string // rawConfigKey will be prepended
30 + payload string
31 + expect string // JSON representation of what the whole config is expected to be after the request
32 + shouldErr bool
33 + }{
34 + {
35 + method: "POST",
36 + path: "",
37 + payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
38 + expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
39 + },
40 + {
41 + method: "POST",
42 + path: "/foo",
43 + payload: `"jet"`,
44 + expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
45 + },
46 + {
47 + method: "POST",
48 + path: "/bar",
49 + payload: `{"aa": "bb", "qq": "zz"}`,
50 + expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
51 + },
52 + {
53 + method: "DELETE",
54 + path: "/bar/qq",
55 + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
56 + },
57 + {
58 + method: "POST",
59 + path: "/list",
60 + payload: `"e"`,
61 + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
62 + },
63 + {
64 + method: "PUT",
65 + path: "/list/3",
66 + payload: `"d"`,
67 + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
68 + },
69 + {
70 + method: "DELETE",
71 + path: "/list/3",
72 + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
73 + },
74 + {
75 + method: "PATCH",
76 + path: "/list/3",
77 + payload: `"d"`,
78 + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
79 + },
80 + } {
81 + err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
82 +
83 + if tc.shouldErr && err == nil {
84 + t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
85 + }
86 + if !tc.shouldErr && err != nil {
87 + t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
88 + }
89 +
90 + // decode the expected config so we can do a convenient DeepEqual
91 + var expectedDecoded interface{}
92 + err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
93 + if err != nil {
94 + t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
95 + }
96 +
97 + // make sure the resulting config is as we expect it
98 + if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
99 + t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
100 + i, expectedDecoded, rawCfg[rawConfigKey])
101 + }
102 + }
103 +}
104 +
105 func BenchmarkLoad(b *testing.B) {
106 for i := 0; i < b.N; i++ {
24 - r := strings.NewReader(`{
25 - "testval": "Yippee!",
107 + cfg := []byte(`{
108 "apps": {
109 "http": {
110 "servers": {
@@ -39,6 +121,6 @@ func BenchmarkLoad(b *testing.B) {
121 }
122 }
123 `)
42 - Load(r)
124 + Load(cfg, true)
125 }
126 }
caddy.go
+229 -18
@@ -15,11 +15,16 @@
15 package caddy
16
17 import (
18 + "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 + "io"
23 "log"
24 + "net/http"
25 + "path"
26 "runtime/debug"
27 + "strconv"
28 "strings"
29 "sync"
30 "time"
@@ -27,7 +32,18 @@ import (
32 "github.com/mholt/certmagic"
33 )
34
30 -// Config represents a Caddy configuration.
35 +// Config represents a Caddy configuration. It is the
36 +// top of the module structure: all Caddy modules will
37 +// be loaded, starting with this struct. In order to
38 +// be loaded and run successfully, a Config and all its
39 +// modules must be JSON-encodable; i.e. when filling a
40 +// Config struct manually, its JSON-encodable fields
41 +// (the ones with JSON struct tags, usually ending with
42 +// "Raw" if they decode into a separate field) must be
43 +// set so that they can be unmarshaled and provisioned.
44 +// Setting the fields for the decoded values instead
45 +// will result in those values being overwritten at
46 +// unmarshaling/provisioning.
47 type Config struct {
48 Admin *AdminConfig `json:"admin,omitempty"`
49 Logging *Logging `json:"logging,omitempty"`
@@ -46,13 +62,164 @@ type App interface {
62 Stop() error
63 }
64
49 -// Run runs Caddy with the given config.
50 -func Run(newCfg *Config) error {
65 +// Run runs the given config, replacing any existing config.
66 +func Run(cfg *Config) error {
67 + cfgJSON, err := json.Marshal(cfg)
68 + if err != nil {
69 + return err
70 + }
71 + return Load(cfgJSON, true)
72 +}
73 +
74 +// Load loads the given config JSON and runs it only
75 +// if it is different from the current config or
76 +// forceReload is true.
77 +func Load(cfgJSON []byte, forceReload bool) error {
78 + return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
79 +}
80 +
81 +// changeConfig changes the current config (rawCfg) according to the
82 +// method, traversed via the given path, and uses the given input as
83 +// the new value (if applicable; i.e. "DELETE" doesn't have an input).
84 +// If the resulting config is the same as the previous, no reload will
85 +// occur unless forceReload is true. This function is safe for
86 +// concurrent use.
87 +func changeConfig(method, path string, input []byte, forceReload bool) error {
88 + switch method {
89 + case http.MethodGet,
90 + http.MethodHead,
91 + http.MethodOptions,
92 + http.MethodConnect,
93 + http.MethodTrace:
94 + return fmt.Errorf("method not allowed")
95 + }
96 +
97 currentCfgMu.Lock()
98 defer currentCfgMu.Unlock()
99
100 + err := unsyncedConfigAccess(method, path, input, nil)
101 + if err != nil {
102 + return err
103 + }
104 +
105 + // find any IDs in this config and index them
106 + idx := make(map[string]string)
107 + err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
108 + if err != nil {
109 + return APIError{
110 + Code: http.StatusInternalServerError,
111 + Err: fmt.Errorf("indexing config: %v", err),
112 + }
113 + }
114 +
115 + // the mutation is complete, so encode the entire config as JSON
116 + newCfg, err := json.Marshal(rawCfg[rawConfigKey])
117 + if err != nil {
118 + return APIError{
119 + Code: http.StatusBadRequest,
120 + Err: fmt.Errorf("encoding new config: %v", err),
121 + }
122 + }
123 +
124 + // if nothing changed, no need to do a whole reload unless the client forces it
125 + if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
126 + Log().Named("admin.api.change_config").Info("config is unchanged")
127 + return nil
128 + }
129 +
130 + // load this new config; if it fails, we need to revert to
131 + // our old representation of caddy's actual config
132 + err = unsyncedDecodeAndRun(newCfg)
133 + if err != nil {
134 + if len(rawCfgJSON) > 0 {
135 + // restore old config state to keep it consistent
136 + // with what caddy is still running; we need to
137 + // unmarshal it again because it's likely that
138 + // pointers deep in our rawCfg map were modified
139 + var oldCfg interface{}
140 + err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
141 + if err2 != nil {
142 + err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
143 + }
144 + rawCfg[rawConfigKey] = oldCfg
145 + }
146 +
147 + return fmt.Errorf("loading new config: %v", err)
148 + }
149 +
150 + // success, so update our stored copy of the encoded
151 + // config to keep it consistent with what caddy is now
152 + // running (storing an encoded copy is not strictly
153 + // necessary, but avoids an extra json.Marshal for
154 + // each config change)
155 + rawCfgJSON = newCfg
156 + rawCfgIndex = idx
157 +
158 + return nil
159 +}
160 +
161 +// readConfig traverses the current config to path
162 +// and writes its JSON encoding to out.
163 +func readConfig(path string, out io.Writer) error {
164 + currentCfgMu.RLock()
165 + defer currentCfgMu.RUnlock()
166 + return unsyncedConfigAccess(http.MethodGet, path, nil, out)
167 +}
168 +
169 +// indexConfigObjects recurisvely searches ptr for object fields named
170 +// "@id" and maps that ID value to the full configPath in the index.
171 +// This function is NOT safe for concurrent access; obtain a write lock
172 +// on currentCfgMu.
173 +func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
174 + switch val := ptr.(type) {
175 + case map[string]interface{}:
176 + for k, v := range val {
177 + if k == idKey {
178 + switch idVal := v.(type) {
179 + case string:
180 + index[idVal] = configPath
181 + case float64: // all JSON numbers decode as float64
182 + index[fmt.Sprintf("%v", idVal)] = configPath
183 + default:
184 + return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
185 + }
186 + delete(val, idKey) // field is no longer needed, and will break config if not removed
187 + continue
188 + }
189 + // traverse this object property recursively
190 + err := indexConfigObjects(val[k], path.Join(configPath, k), index)
191 + if err != nil {
192 + return err
193 + }
194 + }
195 + case []interface{}:
196 + // traverse each element of the array recursively
197 + for i := range val {
198 + err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
199 + if err != nil {
200 + return err
201 + }
202 + }
203 + }
204 +
205 + return nil
206 +}
207 +
208 +// unsyncedDecodeAndRun decodes cfgJSON and runs
209 +// it as the new config, replacing any other
210 +// current config. It does not update the raw
211 +// config state, as this is a lower-level function;
212 +// most callers will want to use Load instead.
213 +// A write lock on currentCfgMu is required!
214 +func unsyncedDecodeAndRun(cfgJSON []byte) error {
215 + var newCfg *Config
216 + err := strictUnmarshalJSON(cfgJSON, &newCfg)
217 + if err != nil {
218 + return err
219 + }
220 +
221 // run the new config and start all its apps
55 - err := run(newCfg, true)
222 + err = run(newCfg, true)
223 if err != nil {
224 return err
225 }
@@ -77,11 +244,11 @@ func Run(newCfg *Config) error {
244 // the config if you are not going to start it,
245 // so that each provisioned module will be
246 // cleaned up.
247 +//
248 +// This is a low-level function; most callers
249 +// will want to use Run instead, which also
250 +// updates the config's raw state.
251 func run(newCfg *Config, start bool) error {
81 - if newCfg == nil {
82 - return nil
83 - }
84 -
252 // because we will need to roll back any state
253 // modifications if this function errors, we
254 // keep a single error value and scope all
@@ -91,6 +258,16 @@ func run(newCfg *Config, start bool) error {
258 // been set by a short assignment
259 var err error
260
261 + // start the admin endpoint (and stop any prior one)
262 + err = replaceAdmin(newCfg)
263 + if err != nil {
264 + return fmt.Errorf("starting caddy administration endpoint: %v", err)
265 + }
266 +
267 + if newCfg == nil {
268 + return nil
269 + }
270 +
271 // prepare the new config for use
272 newCfg.apps = make(map[string]App)
273
@@ -198,22 +375,25 @@ func run(newCfg *Config, start bool) error {
375 // It is the antithesis of Run(). This function
376 // will log any errors that occur during the
377 // stopping of individual apps and continue to
201 -// stop the others.
378 +// stop the others. Stop should only be called
379 +// if not replacing with a new config.
380 func Stop() error {
381 currentCfgMu.Lock()
382 defer currentCfgMu.Unlock()
383 unsyncedStop(currentCfg)
384 currentCfg = nil
385 + rawCfgJSON = nil
386 + rawCfgIndex = nil
387 + rawCfg[rawConfigKey] = nil
388 return nil
389 }
390
210 -// unsyncedStop stops cfg from running, but if
211 -// applicable, you need to acquire locks yourself.
212 -// It is a no-op if cfg is nil. If any app
213 -// returns an error when stopping, it is logged
214 -// and the function continues with the next app.
215 -// This function assumes all apps in cfg were
216 -// successfully started.
391 +// unsyncedStop stops cfg from running, but has
392 +// no locking around cfg. It is a no-op if cfg is
393 +// nil. If any app returns an error when stopping,
394 +// it is logged and the function continues stopping
395 +// the next app. This function assumes all apps in
396 +// cfg were successfully started first.
397 func unsyncedStop(cfg *Config) {
398 if cfg == nil {
399 return
@@ -231,6 +411,17 @@ func unsyncedStop(cfg *Config) {
411 cfg.cancelFunc()
412 }
413
414 +// stopAndCleanup calls stop and cleans up anything
415 +// else that is expedient. This should only be used
416 +// when stopping and not replacing with a new config.
417 +func stopAndCleanup() error {
418 + if err := Stop(); err != nil {
419 + return err
420 + }
421 + certmagic.CleanUpOwnLocks()
422 + return nil
423 +}
424 +
425 // Validate loads, provisions, and validates
426 // cfg, but does not start running it.
427 func Validate(cfg *Config) error {
@@ -289,8 +480,28 @@ func goModule(mod *debug.Module) *debug.Module {
480 // CtxKey is a value type for use with context.WithValue.
481 type CtxKey string
482
292 -// currentCfg is the currently-loaded configuration.
483 +// This group of variables pertains to the current configuration.
484 var (
294 - currentCfg *Config
485 + // currentCfgMu protects everything in this var block.
486 currentCfgMu sync.RWMutex
487 +
488 + // currentCfg is the currently-running configuration.
489 + currentCfg *Config
490 +
491 + // rawCfg is the current, generic-decoded configuration;
492 + // we initialize it as a map with one field ("config")
493 + // to maintain parity with the API endpoint and to avoid
494 + // the special case of having to access/mutate the variable
495 + // directly without traversing into it.
496 + rawCfg = map[string]interface{}{
497 + rawConfigKey: nil,
498 + }
499 +
500 + // rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
501 + // this around avoids an extra Marshal call during changes.
502 + rawCfgJSON []byte
503 +
504 + // rawCfgIndex is the map of user-assigned ID to expanded
505 + // path, for converting /id/ paths to /config/ paths.
506 + rawCfgIndex map[string]string
507 )
cmd/commandfuncs.go
+4 -15
@@ -160,23 +160,12 @@ func cmdRun(fl Flags) (int, error) {
160 cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
161 certmagic.UserAgent = "Caddy/" + cleanModVersion
162
163 - // start the admin endpoint along with any initial config
164 - // a configuration without admin config is considered fine
165 - // but does not enable the admin endpoint at all
166 - err = caddy.StartAdmin(config)
167 - if err == nil {
168 - defer caddy.StopAdmin()
169 - } else if err != caddy.ErrAdminInterfaceNotConfigured {
170 - return caddy.ExitCodeFailedStartup,
171 - fmt.Errorf("starting caddy administration endpoint: %v", err)
163 + // run the initial config
164 + err = caddy.Load(config, true)
165 + if err != nil {
166 + return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
167 }
173 -
174 - // if a config has been supplied, load it as initial config
168 if len(config) > 0 {
176 - err := caddy.Load(bytes.NewReader(config))
177 - if err != nil {
178 - return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
179 - }
169 caddy.Log().Named("admin").Info("Caddy 2 serving initial configuration")
170 }
171
dynamicconfig.go deleted
-358
@@ -1,358 +0,0 @@
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 caddy
16 -
17 -import (
18 - "bytes"
19 - "encoding/json"
20 - "fmt"
21 - "log"
22 - "net/http"
23 - "path"
24 - "strconv"
25 - "strings"
26 - "sync"
27 -)
28 -
29 -func init() {
30 - RegisterModule(router{})
31 -}
32 -
33 -type router []AdminRoute
34 -
35 -// CaddyModule returns the Caddy module information.
36 -func (router) CaddyModule() ModuleInfo {
37 - return ModuleInfo{
38 - Name: "admin.routers.dynamic_config",
39 - New: func() Module {
40 - return router{
41 - {
42 - Pattern: "/" + rawConfigKey + "/",
43 - Handler: http.HandlerFunc(handleConfig),
44 - },
45 - {
46 - Pattern: "/id/",
47 - Handler: http.HandlerFunc(handleConfigID),
48 - },
49 - }
50 - },
51 - }
52 -}
53 -
54 -func (r router) Routes() []AdminRoute { return r }
55 -
56 -// handleConfig handles config changes or exports according to r.
57 -// This function is safe for concurrent use.
58 -func handleConfig(w http.ResponseWriter, r *http.Request) {
59 - rawCfgMu.Lock()
60 - defer rawCfgMu.Unlock()
61 - unsyncedHandleConfig(w, r)
62 -}
63 -
64 -// handleConfigID accesses the config through a user-assigned ID
65 -// that is mapped to its full/expanded path in the JSON structure.
66 -// It is the same as handleConfig except it replaces the ID in
67 -// the request path with the full, expanded URL path.
68 -// This function is safe for concurrent use.
69 -func handleConfigID(w http.ResponseWriter, r *http.Request) {
70 - parts := strings.Split(r.URL.Path, "/")
71 - if len(parts) < 3 || parts[2] == "" {
72 - http.Error(w, "request path is missing object ID", http.StatusBadRequest)
73 - return
74 - }
75 - id := parts[2]
76 -
77 - rawCfgMu.Lock()
78 - defer rawCfgMu.Unlock()
79 -
80 - // map the ID to the expanded path
81 - expanded, ok := rawCfgIndex[id]
82 - if !ok {
83 - http.Error(w, "unknown object ID: "+id, http.StatusBadRequest)
84 - return
85 - }
86 -
87 - // piece the full URL path back together
88 - parts = append([]string{expanded}, parts[3:]...)
89 - r.URL.Path = path.Join(parts...)
90 -
91 - unsyncedHandleConfig(w, r)
92 -}
93 -
94 -// configIndex recurisvely searches ptr for object fields named "@id"
95 -// and maps that ID value to the full configPath in the index.
96 -// This function is NOT safe for concurrent access; use rawCfgMu.
97 -func configIndex(ptr interface{}, configPath string, index map[string]string) error {
98 - switch val := ptr.(type) {
99 - case map[string]interface{}:
100 - for k, v := range val {
101 - if k == "@id" {
102 - switch idVal := v.(type) {
103 - case string:
104 - index[idVal] = configPath
105 - case float64: // all JSON numbers decode as float64
106 - index[fmt.Sprintf("%v", idVal)] = configPath
107 - default:
108 - return fmt.Errorf("%s: @id field must be a string or number", configPath)
109 - }
110 - delete(val, "@id") // field is no longer needed, and will break config if not removed
111 - continue
112 - }
113 - // traverse this object property recursively
114 - err := configIndex(val[k], path.Join(configPath, k), index)
115 - if err != nil {
116 - return err
117 - }
118 - }
119 - case []interface{}:
120 - // traverse each element of the array recursively
121 - for i := range val {
122 - err := configIndex(val[i], path.Join(configPath, strconv.Itoa(i)), index)
123 - if err != nil {
124 - return err
125 - }
126 - }
127 - }
128 -
129 - return nil
130 -}
131 -
132 -// unsycnedHandleConfig handles config accesses without a lock
133 -// on rawCfgMu. This is NOT safe for concurrent use, so be sure
134 -// to acquire a lock on rawCfgMu before calling this.
135 -func unsyncedHandleConfig(w http.ResponseWriter, r *http.Request) {
136 - switch r.Method {
137 - case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
138 - default:
139 - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
140 - return
141 - }
142 -
143 - // perform the mutation with our decoded representation
144 - // (the map), which may change pointers deep within it
145 - err := mutateConfig(w, r)
146 - if err != nil {
147 - http.Error(w, "mutating config: "+err.Error(), http.StatusBadRequest)
148 - return
149 - }
150 -
151 - if r.Method != http.MethodGet {
152 - // find any IDs in this config and index them
153 - idx := make(map[string]string)
154 - err = configIndex(rawCfg[rawConfigKey], "/config", idx)
155 - if err != nil {
156 - http.Error(w, "indexing config: "+err.Error(), http.StatusInternalServerError)
157 - return
158 - }
159 -
160 - // the mutation is complete, so encode the entire config as JSON
161 - newCfg, err := json.Marshal(rawCfg[rawConfigKey])
162 - if err != nil {
163 - http.Error(w, "encoding new config: "+err.Error(), http.StatusBadRequest)
164 - return
165 - }
166 -
167 - // if nothing changed, no need to do a whole reload unless the client forces it
168 - if r.Header.Get("Cache-Control") != "must-revalidate" && bytes.Equal(rawCfgJSON, newCfg) {
169 - log.Printf("[ADMIN][INFO] Config is unchanged")
170 - return
171 - }
172 -
173 - // load this new config; if it fails, we need to revert to
174 - // our old representation of caddy's actual config
175 - err = Load(bytes.NewReader(newCfg))
176 - if err != nil {
177 - // restore old config state to keep it consistent
178 - // with what caddy is still running; we need to
179 - // unmarshal it again because it's likely that
180 - // pointers deep in our rawCfg map were modified
181 - var oldCfg interface{}
182 - err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
183 - if err2 != nil {
184 - err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
185 - }
186 - rawCfg[rawConfigKey] = oldCfg
187 -
188 - // report error
189 - log.Printf("[ADMIN][ERROR] loading config: %v", err)
190 - http.Error(w, err.Error(), http.StatusBadRequest)
191 - return
192 - }
193 -
194 - // success, so update our stored copy of the encoded
195 - // config to keep it consistent with what caddy is now
196 - // running (storing an encoded copy is not strictly
197 - // necessary, but avoids an extra json.Marshal for
198 - // each config change)
199 - rawCfgJSON = newCfg
200 - rawCfgIndex = idx
201 - }
202 -}
203 -
204 -// mutateConfig changes the rawCfg according to r. It is NOT
205 -// safe for concurrent use; use rawCfgMu. If the request's
206 -// method is GET, the config will not be changed.
207 -func mutateConfig(w http.ResponseWriter, r *http.Request) error {
208 - var err error
209 - var val interface{}
210 -
211 - // if there is a request body, make sure we recognize its content-type and decode it
212 - if r.Method != http.MethodGet && r.Method != http.MethodDelete {
213 - if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
214 - return fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct)
215 - }
216 - err = json.NewDecoder(r.Body).Decode(&val)
217 - if err != nil {
218 - return fmt.Errorf("decoding request body: %v", err)
219 - }
220 - }
221 -
222 - buf := new(bytes.Buffer)
223 - enc := json.NewEncoder(buf)
224 -
225 - cleanPath := strings.Trim(r.URL.Path, "/")
226 - if cleanPath == "" {
227 - return fmt.Errorf("no traversable path")
228 - }
229 -
230 - parts := strings.Split(cleanPath, "/")
231 - if len(parts) == 0 {
232 - return fmt.Errorf("path missing")
233 - }
234 -
235 - var ptr interface{} = rawCfg
236 -
237 -traverseLoop:
238 - for i, part := range parts {
239 - switch v := ptr.(type) {
240 - case map[string]interface{}:
241 - // if the next part enters a slice, and the slice is our destination,
242 - // handle it specially (because appending to the slice copies the slice
243 - // header, which does not replace the original one like we want)
244 - if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
245 - var idx int
246 - if r.Method != http.MethodPost {
247 - idxStr := parts[len(parts)-1]
248 - idx, err = strconv.Atoi(idxStr)
249 - if err != nil {
250 - return fmt.Errorf("[%s] invalid array index '%s': %v",
251 - r.URL.Path, idxStr, err)
252 - }
253 - if idx < 0 || idx >= len(arr) {
254 - return fmt.Errorf("[%s] array index out of bounds: %s", r.URL.Path, idxStr)
255 - }
256 - }
257 -
258 - switch r.Method {
259 - case http.MethodGet:
260 - err = enc.Encode(arr[idx])
261 - if err != nil {
262 - return fmt.Errorf("encoding config: %v", err)
263 - }
264 - case http.MethodPost:
265 - v[part] = append(arr, val)
266 - case http.MethodPut:
267 - // avoid creation of new slice and a second copy (see
268 - // https://github.com/golang/go/wiki/SliceTricks#insert)
269 - arr = append(arr, nil)
270 - copy(arr[idx+1:], arr[idx:])
271 - arr[idx] = val
272 - v[part] = arr
273 - case http.MethodPatch:
274 - arr[idx] = val
275 - case http.MethodDelete:
276 - v[part] = append(arr[:idx], arr[idx+1:]...)
277 - default:
278 - return fmt.Errorf("unrecognized method %s", r.Method)
279 - }
280 - break traverseLoop
281 - }
282 -
283 - if i == len(parts)-1 {
284 - switch r.Method {
285 - case http.MethodGet:
286 - err = enc.Encode(v[part])
287 - if err != nil {
288 - return fmt.Errorf("encoding config: %v", err)
289 - }
290 - case http.MethodPost:
291 - if arr, ok := v[part].([]interface{}); ok {
292 - // if the part is an existing list, POST appends to it
293 - // TODO: Do we ever reach this point, since we handle arrays
294 - // separately above?
295 - v[part] = append(arr, val)
296 - } else {
297 - // otherwise, it simply sets the value
298 - v[part] = val
299 - }
300 - case http.MethodPut:
301 - if _, ok := v[part]; ok {
302 - return fmt.Errorf("[%s] key already exists: %s", r.URL.Path, part)
303 - }
304 - v[part] = val
305 - case http.MethodPatch:
306 - if _, ok := v[part]; !ok {
307 - return fmt.Errorf("[%s] key does not exist: %s", r.URL.Path, part)
308 - }
309 - v[part] = val
310 - case http.MethodDelete:
311 - delete(v, part)
312 - default:
313 - return fmt.Errorf("unrecognized method %s", r.Method)
314 - }
315 - } else {
316 - ptr = v[part]
317 - }
318 -
319 - case []interface{}:
320 - partInt, err := strconv.Atoi(part)
321 - if err != nil {
322 - return fmt.Errorf("[/%s] invalid array index '%s': %v",
323 - strings.Join(parts[:i+1], "/"), part, err)
324 - }
325 - if partInt < 0 || partInt >= len(v) {
326 - return fmt.Errorf("[/%s] array index out of bounds: %s",
327 - strings.Join(parts[:i+1], "/"), part)
328 - }
329 - ptr = v[partInt]
330 -
331 - default:
332 - return fmt.Errorf("invalid path: %s", parts[:i+1])
333 - }
334 - }
335 -
336 - if r.Method == http.MethodGet {
337 - w.Header().Set("Content-Type", "application/json")
338 - w.Write(buf.Bytes())
339 - }
340 -
341 - return nil
342 -}
343 -
344 -var (
345 - // rawCfg is the current, generic-decoded configuration;
346 - // we initialize it as a map with one field ("config")
347 - // to maintain parity with the API endpoint and to avoid
348 - // the special case of having to access/mutate the variable
349 - // directly without traversing into it
350 - rawCfg = map[string]interface{}{
351 - rawConfigKey: nil,
352 - }
353 - rawCfgJSON []byte // keeping the encoded form avoids an extra Marshal on changes
354 - rawCfgIndex map[string]string // map of user-assigned ID to expanded path
355 - rawCfgMu sync.Mutex // protects rawCfg, rawCfgJSON, and rawCfgIndex
356 -)
357 -
358 -const rawConfigKey = "config"
dynamicconfig_test.go deleted
-123
@@ -1,123 +0,0 @@
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 caddy
16 -
17 -import (
18 - "encoding/json"
19 - "net/http"
20 - "net/http/httptest"
21 - "reflect"
22 - "strings"
23 - "testing"
24 -)
25 -
26 -func TestMutateConfig(t *testing.T) {
27 - // each test is performed in sequence, so
28 - // each change builds on the previous ones;
29 - // the config is not reset between tests
30 - for i, tc := range []struct {
31 - method string
32 - path string // rawConfigKey will be prepended
33 - payload string
34 - expect string // JSON representation of what the whole config is expected to be after the request
35 - shouldErr bool
36 - }{
37 - {
38 - method: "POST",
39 - path: "",
40 - payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
41 - expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
42 - },
43 - {
44 - method: "POST",
45 - path: "/foo",
46 - payload: `"jet"`,
47 - expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
48 - },
49 - {
50 - method: "POST",
51 - path: "/bar",
52 - payload: `{"aa": "bb", "qq": "zz"}`,
53 - expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
54 - },
55 - {
56 - method: "DELETE",
57 - path: "/bar/qq",
58 - expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
59 - },
60 - {
61 - method: "POST",
62 - path: "/list",
63 - payload: `"e"`,
64 - expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
65 - },
66 - {
67 - method: "PUT",
68 - path: "/list/3",
69 - payload: `"d"`,
70 - expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
71 - },
72 - {
73 - method: "DELETE",
74 - path: "/list/3",
75 - expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
76 - },
77 - {
78 - method: "PATCH",
79 - path: "/list/3",
80 - payload: `"d"`,
81 - expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
82 - },
83 - } {
84 - req, err := http.NewRequest(tc.method, rawConfigKey+tc.path, strings.NewReader(tc.payload))
85 - if err != nil {
86 - t.Fatalf("Test %d: making test request: %v", i, err)
87 - }
88 - req.Header.Set("Content-Type", "application/json")
89 -
90 - w := httptest.NewRecorder()
91 -
92 - err = mutateConfig(w, req)
93 -
94 - if tc.shouldErr && err == nil {
95 - t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
96 - }
97 - if !tc.shouldErr && err != nil {
98 - t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
99 - }
100 -
101 - if tc.shouldErr && w.Code == http.StatusOK {
102 - t.Fatalf("Test %d: Expected error, but got HTTP %d: %s",
103 - i, w.Code, w.Body.String())
104 - }
105 - if !tc.shouldErr && w.Code != http.StatusOK {
106 - t.Fatalf("Test %d: Should not have errored, but got HTTP %d: %s",
107 - i, w.Code, w.Body.String())
108 - }
109 -
110 - // decode the expected config so we can do a convenient DeepEqual
111 - var expectedDecoded interface{}
112 - err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
113 - if err != nil {
114 - t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
115 - }
116 -
117 - // make sure the resulting config is as we expect it
118 - if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
119 - t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
120 - i, expectedDecoded, rawCfg[rawConfigKey])
121 - }
122 - }
123 -}
go.mod
-3
@@ -6,7 +6,6 @@ require (
6 github.com/Masterminds/sprig/v3 v3.0.0
7 github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb
8 github.com/dustin/go-humanize v1.0.0
9 - github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 // indirect
9 github.com/go-acme/lego/v3 v3.1.0
10 github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
11 github.com/ilibs/json5 v1.0.1
@@ -20,7 +19,6 @@ require (
19 github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6
20 github.com/onsi/ginkgo v1.8.0 // indirect
21 github.com/onsi/gomega v1.5.0 // indirect
23 - github.com/rs/cors v1.7.0
22 github.com/russross/blackfriday/v2 v2.0.1
23 github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
24 github.com/vulcand/oxy v1.0.0
@@ -29,6 +27,5 @@ require (
27 go.uber.org/zap v1.10.0
28 golang.org/x/crypto v0.0.0-20191010185427-af544f31c8ac
29 golang.org/x/net v0.0.0-20191009170851-d66e71096ffb
32 - golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0
30 gopkg.in/natefinch/lumberjack.v2 v2.0.0
31 )
go.sum
-4
@@ -60,8 +60,6 @@ github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s9
60 github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
61 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
62 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
63 -github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 h1:3WV5aRRj1ELP3RcLlBp/v0WJTuy47OQMkL9GIQq8QEE=
64 -github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
63 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
64 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
65 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -236,8 +234,6 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
234 github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
235 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
236 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
239 -github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
240 -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
237 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
238 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
239 github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=