@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 218 lines 6.27 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 "crypto/tls"
19 "crypto/x509"
20 "fmt"
21 "io"
22 "net/http"
23 "os"
24 "time"
25
26 "github.com/caddyserver/caddy/v2"
27 )
28
29 func init() {
30 caddy.RegisterModule(HTTPLoader{})
31 }
32
33 // HTTPLoader can load Caddy configs over HTTP(S).
34 //
35 // If the response is not a JSON config, a config adapter must be specified
36 // either in the loader config (`adapter`), or in the Content-Type HTTP header
37 // returned in the HTTP response from the server. The Content-Type header is
38 // read just like the admin API's `/load` endpoint. If you don't have control
39 // over the HTTP server (but can still trust its response), you can override
40 // the Content-Type header by setting the `adapter` property in this config.
41 type HTTPLoader struct {
42 // The method for the request. Default: GET
43 Method string `json:"method,omitempty"`
44
45 // The URL of the request.
46 URL string `json:"url,omitempty"`
47
48 // HTTP headers to add to the request.
49 Headers http.Header `json:"header,omitempty"`
50
51 // Maximum time allowed for a complete connection and request.
52 Timeout caddy.Duration `json:"timeout,omitempty"`
53
54 // The name of the config adapter to use, if any. Only needed
55 // if the HTTP response is not a JSON config and if the server's
56 // Content-Type header is missing or incorrect.
57 Adapter string `json:"adapter,omitempty"`
58
59 TLS *struct {
60 // Present this instance's managed remote identity credentials to the server.
61 UseServerIdentity bool `json:"use_server_identity,omitempty"`
62
63 // PEM-encoded client certificate filename to present to the server.
64 ClientCertificateFile string `json:"client_certificate_file,omitempty"`
65
66 // PEM-encoded key to use with the client certificate.
67 ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
68
69 // List of PEM-encoded CA certificate files to add to the same trust
70 // store as RootCAPool (or root_ca_pool in the JSON).
71 RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
72 } `json:"tls,omitempty"`
73 }
74
75 // CaddyModule returns the Caddy module information.
76 func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
77 return caddy.ModuleInfo{
78 ID: "caddy.config_loaders.http",
79 New: func() caddy.Module { return new(HTTPLoader) },
80 }
81 }
82
83 // LoadConfig loads a Caddy config.
84 func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
85 repl := caddy.NewReplacer()
86
87 client, err := hl.makeClient(ctx)
88 if err != nil {
89 return nil, err
90 }
91
92 method := repl.ReplaceAll(hl.Method, "")
93 if method == "" {
94 method = http.MethodGet
95 }
96
97 url := repl.ReplaceAll(hl.URL, "")
98 req, err := http.NewRequestWithContext(ctx, method, url, nil)
99 if err != nil {
100 return nil, err
101 }
102 for key, vals := range hl.Headers {
103 for _, val := range vals {
104 req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
105 }
106 }
107
108 resp, err := doHttpCallWithRetries(ctx, client, req)
109 if err != nil {
110 return nil, err
111 }
112 defer resp.Body.Close()
113 if resp.StatusCode >= 400 {
114 return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
115 }
116
117 body, err := io.ReadAll(resp.Body)
118 if err != nil {
119 return nil, err
120 }
121
122 // adapt the config based on either manually-configured adapter or server's response header
123 ct := resp.Header.Get("Content-Type")
124 if hl.Adapter != "" {
125 ct = "text/" + hl.Adapter
126 }
127 result, warnings, err := adaptByContentType(ct, body)
128 if err != nil {
129 return nil, err
130 }
131 for _, warn := range warnings {
132 ctx.Logger().Warn(warn.String())
133 }
134
135 return result, nil
136 }
137
138 func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
139 resp, err := client.Do(request) //nolint:gosec // no SSRF; comes from trusted config
140 if err != nil {
141 return nil, fmt.Errorf("problem calling http loader url: %v", err)
142 } else if resp.StatusCode < 200 || resp.StatusCode > 499 {
143 resp.Body.Close()
144 return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
145 }
146 return resp, nil
147 }
148
149 func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
150 var resp *http.Response
151 var err error
152 const maxAttempts = 10
153
154 for i := range maxAttempts {
155 resp, err = attemptHttpCall(client, request)
156 if err != nil && i < maxAttempts-1 {
157 select {
158 case <-time.After(time.Millisecond * 500):
159 case <-ctx.Done():
160 return resp, ctx.Err()
161 }
162 } else {
163 break
164 }
165 }
166
167 return resp, err
168 }
169
170 func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
171 client := &http.Client{
172 Timeout: time.Duration(hl.Timeout),
173 }
174
175 if hl.TLS != nil {
176 var tlsConfig *tls.Config
177
178 // client authentication
179 if hl.TLS.UseServerIdentity {
180 certs, err := ctx.IdentityCredentials(ctx.Logger())
181 if err != nil {
182 return nil, fmt.Errorf("getting server identity credentials: %v", err)
183 }
184 // See https://github.com/securego/gosec/issues/1054#issuecomment-2072235199
185 //nolint:gosec
186 tlsConfig = &tls.Config{Certificates: certs}
187 } else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
188 cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
189 if err != nil {
190 return nil, err
191 }
192 //nolint:gosec
193 tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
194 }
195
196 // trusted server certs
197 if len(hl.TLS.RootCAPEMFiles) > 0 {
198 rootPool := x509.NewCertPool()
199 for _, pemFile := range hl.TLS.RootCAPEMFiles {
200 pemData, err := os.ReadFile(pemFile)
201 if err != nil {
202 return nil, fmt.Errorf("failed reading ca cert: %v", err)
203 }
204 rootPool.AppendCertsFromPEM(pemData)
205 }
206 if tlsConfig == nil {
207 tlsConfig = new(tls.Config)
208 }
209 tlsConfig.RootCAs = rootPool
210 }
211
212 client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
213 }
214
215 return client, nil
216 }
217
218 var _ caddy.ConfigLoader = (*HTTPLoader)(nil)