From 37093befd59107316d422fcd3cc011b2481cd2cf Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sun, 22 Mar 2020 01:49:10 +0300 Subject: caddyconfig: register adapters as Caddy modules (#3132) * admin: Refactor /load endpoint out of caddy package This eliminates the caddy package's dependency on the caddyconfig package, which helps prevent import cycles. * v2: adapter: register config adapters as Caddy modules * v2: adapter: simplify adapter registration as adapters and modules * v2: adapter: let RegisterAdapter be in charge of registering adapters as modules * v2: adapter: remove underscrores placeholders * v2: adapter: explicitly ignore the error of writing response of writing warnings back to client * Implicitly wrap config adapters as modules Co-authored-by: Matthew Holt --- admin.go | 83 ----------------------- caddyconfig/configadapters.go | 21 +++++- caddyconfig/load.go | 153 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 84 deletions(-) create mode 100644 caddyconfig/load.go diff --git a/admin.go b/admin.go index 7a5d0b6..212637f 100644 --- a/admin.go +++ b/admin.go @@ -21,7 +21,6 @@ import ( "expvar" "fmt" "io" - "mime" "net/http" "net/http/pprof" "net/url" @@ -33,7 +32,6 @@ import ( "sync" "time" - "github.com/caddyserver/caddy/v2/caddyconfig" "go.uber.org/zap" ) @@ -115,7 +113,6 @@ func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler { } // register standard config control endpoints - addRoute("/load", AdminHandlerFunc(handleLoad)) addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig)) addRoute("/id/", AdminHandlerFunc(handleConfigID)) addRoute("/stop", AdminHandlerFunc(handleStop)) @@ -407,86 +404,6 @@ func (h adminHandler) originAllowed(origin string) bool { return false } -func handleLoad(w http.ResponseWriter, r *http.Request) error { - if r.Method != http.MethodPost { - return APIError{ - Code: http.StatusMethodNotAllowed, - Err: fmt.Errorf("method not allowed"), - } - } - - buf := bufPool.Get().(*bytes.Buffer) - buf.Reset() - defer bufPool.Put(buf) - - _, err := io.Copy(buf, r.Body) - if err != nil { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("reading request body: %v", err), - } - } - body := buf.Bytes() - - // if the config is formatted other than Caddy's native - // JSON, we need to adapt it before loading it - if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" { - ct, _, err := mime.ParseMediaType(ctHeader) - if err != nil { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("invalid Content-Type: %v", err), - } - } - if !strings.HasSuffix(ct, "/json") { - slashIdx := strings.Index(ct, "/") - if slashIdx < 0 { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("malformed Content-Type"), - } - } - adapterName := ct[slashIdx+1:] - cfgAdapter := caddyconfig.GetAdapter(adapterName) - if cfgAdapter == nil { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName), - } - } - result, warnings, err := cfgAdapter.Adapt(body, nil) - if err != nil { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err), - } - } - if len(warnings) > 0 { - respBody, err := json.Marshal(warnings) - if err != nil { - Log().Named("admin.api.load").Error(err.Error()) - } - w.Write(respBody) - } - body = result - } - } - - forceReload := r.Header.Get("Cache-Control") == "must-revalidate" - - err = Load(body, forceReload) - if err != nil { - return APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("loading config: %v", err), - } - } - - Log().Named("admin.api").Info("load complete") - - return nil -} - func handleConfig(w http.ResponseWriter, r *http.Request) error { switch r.Method { case http.MethodGet: diff --git a/caddyconfig/configadapters.go b/caddyconfig/configadapters.go index 071221f..96d7e10 100644 --- a/caddyconfig/configadapters.go +++ b/caddyconfig/configadapters.go @@ -17,6 +17,8 @@ package caddyconfig import ( "encoding/json" "fmt" + + "github.com/caddyserver/caddy/v2" ) // Adapter is a type which can adapt a configuration to Caddy JSON. @@ -105,7 +107,7 @@ func RegisterAdapter(name string, adapter Adapter) error { return fmt.Errorf("%s: already registered", name) } configAdapters[name] = adapter - return nil + return caddy.RegisterModule(adapterModule{name, adapter}) } // GetAdapter returns the adapter with the given name, @@ -114,4 +116,21 @@ func GetAdapter(name string) Adapter { return configAdapters[name] } +// adapterModule is a wrapper type that can turn any config +// adapter into a Caddy module, which has the benefit of being +// counted with other modules, even though they do not +// technically extend the Caddy configuration structure. +// See caddyserver/caddy#3132. +type adapterModule struct { + name string + Adapter +} + +func (am adapterModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: caddy.ModuleID("caddy.adapters." + am.name), + New: func() caddy.Module { return am }, + } +} + var configAdapters = make(map[string]Adapter) diff --git a/caddyconfig/load.go b/caddyconfig/load.go new file mode 100644 index 0000000..4855b46 --- /dev/null +++ b/caddyconfig/load.go @@ -0,0 +1,153 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyconfig + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "strings" + "sync" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(adminLoad{}) +} + +// adminLoad is a module that provides the /load endpoint +// for the Caddy admin API. The only reason it's not baked +// into the caddy package directly is because of the import +// of the caddyconfig package for its GetAdapter function. +// If the caddy package depends on the caddyconfig package, +// then the caddyconfig package will not be able to import +// the caddy package, and it can more easily cause backward +// edges in the dependency tree (i.e. import cycle). +// Fortunately, the admin API has first-class support for +// adding endpoints from modules. +type adminLoad struct{} + +// CaddyModule returns the Caddy module information. +func (adminLoad) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "admin.api.load", + New: func() caddy.Module { return new(adminLoad) }, + } +} + +// Routes returns a route for the /load endpoint. +func (al adminLoad) Routes() []caddy.AdminRoute { + return []caddy.AdminRoute{ + { + Pattern: "/load", + Handler: caddy.AdminHandlerFunc(al.handleLoad), + }, + } +} + +// handleLoad replaces the entire current configuration with +// a new one provided in the response body. It supports config +// adapters through the use of the Content-Type header. A +// config that is identical to the currently-running config +// will be a no-op unless Cache-Control: must-revalidate is set. +func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error { + if r.Method != http.MethodPost { + return caddy.APIError{ + Code: http.StatusMethodNotAllowed, + Err: fmt.Errorf("method not allowed"), + } + } + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + _, err := io.Copy(buf, r.Body) + if err != nil { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("reading request body: %v", err), + } + } + body := buf.Bytes() + + // if the config is formatted other than Caddy's native + // JSON, we need to adapt it before loading it + if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" { + ct, _, err := mime.ParseMediaType(ctHeader) + if err != nil { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("invalid Content-Type: %v", err), + } + } + if !strings.HasSuffix(ct, "/json") { + slashIdx := strings.Index(ct, "/") + if slashIdx < 0 { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("malformed Content-Type"), + } + } + adapterName := ct[slashIdx+1:] + cfgAdapter := GetAdapter(adapterName) + if cfgAdapter == nil { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName), + } + } + result, warnings, err := cfgAdapter.Adapt(body, nil) + if err != nil { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err), + } + } + if len(warnings) > 0 { + respBody, err := json.Marshal(warnings) + if err != nil { + caddy.Log().Named("admin.api.load").Error(err.Error()) + } + _, _ = w.Write(respBody) + } + body = result + } + } + + forceReload := r.Header.Get("Cache-Control") == "must-revalidate" + + err = caddy.Load(body, forceReload) + if err != nil { + return caddy.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("loading config: %v", err), + } + } + + caddy.Log().Named("admin.api").Info("load complete") + + return nil +} + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} -- cgit v1.2.3