summaryrefslogtreecommitdiff
path: root/caddyconfig
diff options
context:
space:
mode:
Diffstat (limited to 'caddyconfig')
-rw-r--r--caddyconfig/configadapters.go8
-rw-r--r--caddyconfig/httploader.go151
-rw-r--r--caddyconfig/load.go93
3 files changed, 214 insertions, 38 deletions
diff --git a/caddyconfig/configadapters.go b/caddyconfig/configadapters.go
index 1665fa0..ccac5f8 100644
--- a/caddyconfig/configadapters.go
+++ b/caddyconfig/configadapters.go
@@ -35,6 +35,14 @@ type Warning struct {
Message string `json:"message,omitempty"`
}
+func (w Warning) String() string {
+ var directive string
+ if w.Directive != "" {
+ directive = fmt.Sprintf(" (%s)", w.Directive)
+ }
+ return fmt.Sprintf("%s:%d%s: %s", w.File, w.Line, directive, w.Message)
+}
+
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
// marshaling errors (which are highly unlikely with correct code)
// are converted to warnings. This is convenient when filling config
diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go
new file mode 100644
index 0000000..aabd103
--- /dev/null
+++ b/caddyconfig/httploader.go
@@ -0,0 +1,151 @@
+package caddyconfig
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "time"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(HTTPLoader{})
+}
+
+// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
+// based on the Content-Type header of the HTTP response.
+type HTTPLoader struct {
+ // The method for the request. Default: GET
+ Method string `json:"method,omitempty"`
+
+ // The URL of the request.
+ URL string `json:"url,omitempty"`
+
+ // HTTP headers to add to the request.
+ Headers http.Header `json:"header,omitempty"`
+
+ // Maximum time allowed for a complete connection and request.
+ Timeout caddy.Duration `json:"timeout,omitempty"`
+
+ TLS *struct {
+ // Present this instance's managed remote identity credentials to the server.
+ UseServerIdentity bool `json:"use_server_identity,omitempty"`
+
+ // PEM-encoded client certificate filename to present to the server.
+ ClientCertificateFile string `json:"client_certificate_file,omitempty"`
+
+ // PEM-encoded key to use with the client certificate.
+ ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
+
+ // List of PEM-encoded CA certificate files to add to the same trust
+ // store as RootCAPool (or root_ca_pool in the JSON).
+ RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
+ } `json:"tls,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "caddy.config_loaders.http",
+ New: func() caddy.Module { return new(HTTPLoader) },
+ }
+}
+
+// LoadConfig loads a Caddy config.
+func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
+ client, err := hl.makeClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ method := hl.Method
+ if method == "" {
+ method = http.MethodGet
+ }
+
+ req, err := http.NewRequest(method, hl.URL, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header = hl.Headers
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
+ if err != nil {
+ return nil, err
+ }
+ for _, warn := range warnings {
+ ctx.Logger(hl).Warn(warn.String())
+ }
+
+ return result, nil
+}
+
+func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
+ client := &http.Client{
+ Timeout: time.Duration(hl.Timeout),
+ }
+
+ if hl.TLS != nil {
+ var tlsConfig *tls.Config
+
+ // client authentication
+ if hl.TLS.UseServerIdentity {
+ certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
+ if err != nil {
+ return nil, fmt.Errorf("getting server identity credentials: %v", err)
+ }
+ if tlsConfig == nil {
+ tlsConfig = new(tls.Config)
+ }
+ tlsConfig.Certificates = certs
+ } else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
+ cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
+ if err != nil {
+ return nil, err
+ }
+ if tlsConfig == nil {
+ tlsConfig = new(tls.Config)
+ }
+ tlsConfig.Certificates = []tls.Certificate{cert}
+ }
+
+ // trusted server certs
+ if len(hl.TLS.RootCAPEMFiles) > 0 {
+ rootPool := x509.NewCertPool()
+ for _, pemFile := range hl.TLS.RootCAPEMFiles {
+ pemData, err := ioutil.ReadFile(pemFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed reading ca cert: %v", err)
+ }
+ rootPool.AppendCertsFromPEM(pemData)
+ }
+ if tlsConfig == nil {
+ tlsConfig = new(tls.Config)
+ }
+ tlsConfig.RootCAs = rootPool
+ }
+
+ client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
+ }
+
+ return client, nil
+}
+
+var _ caddy.ConfigLoader = (*HTTPLoader)(nil)
diff --git a/caddyconfig/load.go b/caddyconfig/load.go
index 4855b46..7a390d0 100644
--- a/caddyconfig/load.go
+++ b/caddyconfig/load.go
@@ -69,8 +69,8 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
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"),
+ HTTPStatus: http.StatusMethodNotAllowed,
+ Err: fmt.Errorf("method not allowed"),
}
}
@@ -81,8 +81,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
_, err := io.Copy(buf, r.Body)
if err != nil {
return caddy.APIError{
- Code: http.StatusBadRequest,
- Err: fmt.Errorf("reading request body: %v", err),
+ HTTPStatus: http.StatusBadRequest,
+ Err: fmt.Errorf("reading request body: %v", err),
}
}
body := buf.Bytes()
@@ -90,45 +90,21 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
// 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)
+ result, warnings, err := adaptByContentType(ctHeader, body)
if err != nil {
return caddy.APIError{
- Code: http.StatusBadRequest,
- Err: fmt.Errorf("invalid Content-Type: %v", err),
+ HTTPStatus: http.StatusBadRequest,
+ Err: 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 len(warnings) > 0 {
+ respBody, err := json.Marshal(warnings)
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)
+ caddy.Log().Named("admin.api.load").Error(err.Error())
}
- body = result
+ _, _ = w.Write(respBody)
}
+ body = result
}
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
@@ -136,8 +112,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
err = caddy.Load(body, forceReload)
if err != nil {
return caddy.APIError{
- Code: http.StatusBadRequest,
- Err: fmt.Errorf("loading config: %v", err),
+ HTTPStatus: http.StatusBadRequest,
+ Err: fmt.Errorf("loading config: %v", err),
}
}
@@ -146,6 +122,47 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
return nil
}
+// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
+// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
+func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
+ // assume JSON as the default
+ if contentType == "" {
+ return body, nil, nil
+ }
+
+ ct, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ return nil, nil, caddy.APIError{
+ HTTPStatus: http.StatusBadRequest,
+ Err: fmt.Errorf("invalid Content-Type: %v", err),
+ }
+ }
+
+ // if already JSON, no need to adapt
+ if strings.HasSuffix(ct, "/json") {
+ return body, nil, nil
+ }
+
+ // adapter name should be suffix of MIME type
+ slashIdx := strings.Index(ct, "/")
+ if slashIdx < 0 {
+ return nil, nil, fmt.Errorf("malformed Content-Type")
+ }
+
+ adapterName := ct[slashIdx+1:]
+ cfgAdapter := GetAdapter(adapterName)
+ if cfgAdapter == nil {
+ return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
+ }
+
+ result, warnings, err := cfgAdapter.Adapt(body, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
+ }
+
+ return result, warnings, nil
+}
+
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)