mirror of https://github.com/caddyserver/caddy
219 lines
6.2 KiB
Go
219 lines
6.2 KiB
Go
// 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 (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(HTTPLoader{})
|
|
}
|
|
|
|
// HTTPLoader can load Caddy configs over HTTP(S).
|
|
//
|
|
// If the response is not a JSON config, a config adapter must be specified
|
|
// either in the loader config (`adapter`), or in the Content-Type HTTP header
|
|
// returned in the HTTP response from the server. The Content-Type header is
|
|
// read just like the admin API's `/load` endpoint. Uf you don't have control
|
|
// over the HTTP server (but can still trust its response), you can override
|
|
// the Content-Type header by setting the `adapter` property in this config.
|
|
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"`
|
|
|
|
// The name of the config adapter to use, if any. Only needed
|
|
// if the HTTP response is not a JSON config and if the server's
|
|
// Content-Type header is missing or incorrect.
|
|
Adapter string `json:"adapter,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) {
|
|
repl := caddy.NewReplacer()
|
|
|
|
client, err := hl.makeClient(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
method := repl.ReplaceAll(hl.Method, "")
|
|
if method == "" {
|
|
method = http.MethodGet
|
|
}
|
|
|
|
url := repl.ReplaceAll(hl.URL, "")
|
|
req, err := http.NewRequest(method, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for key, vals := range hl.Headers {
|
|
for _, val := range vals {
|
|
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
|
|
}
|
|
}
|
|
|
|
resp, err := doHttpCallWithRetries(ctx, client, 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 := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// adapt the config based on either manually-configured adapter or server's response header
|
|
ct := resp.Header.Get("Content-Type")
|
|
if hl.Adapter != "" {
|
|
ct = "text/" + hl.Adapter
|
|
}
|
|
result, warnings, err := adaptByContentType(ct, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, warn := range warnings {
|
|
ctx.Logger().Warn(warn.String())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
|
resp, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
|
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
|
|
var resp *http.Response
|
|
var err error
|
|
const maxAttempts = 10
|
|
|
|
for i := 0; i < maxAttempts; i++ {
|
|
resp, err = attemptHttpCall(client, request)
|
|
if err != nil && i < maxAttempts-1 {
|
|
select {
|
|
case <-time.After(time.Millisecond * 500):
|
|
case <-ctx.Done():
|
|
return resp, ctx.Err()
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
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())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting server identity credentials: %v", err)
|
|
}
|
|
// See https://github.com/securego/gosec/issues/1054#issuecomment-2072235199
|
|
//nolint:gosec
|
|
tlsConfig = &tls.Config{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
|
|
}
|
|
//nolint:gosec
|
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
}
|
|
|
|
// trusted server certs
|
|
if len(hl.TLS.RootCAPEMFiles) > 0 {
|
|
rootPool := x509.NewCertPool()
|
|
for _, pemFile := range hl.TLS.RootCAPEMFiles {
|
|
pemData, err := os.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)
|