@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 11, 01:00 UTC

master
go 254 lines 7.24 KB
Raw
1 // Copyright 2020 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 caddypki
16
17 import (
18 "encoding/json"
19 "fmt"
20 "net/http"
21 "strings"
22
23 "go.uber.org/zap"
24
25 "github.com/caddyserver/caddy/v2"
26 )
27
28 func init() {
29 caddy.RegisterModule(adminAPI{})
30 }
31
32 // adminAPI is a module that serves PKI endpoints to retrieve
33 // information about the CAs being managed by Caddy.
34 type adminAPI struct {
35 ctx caddy.Context
36 log *zap.Logger
37 pkiApp *PKI
38 }
39
40 // CaddyModule returns the Caddy module information.
41 func (adminAPI) CaddyModule() caddy.ModuleInfo {
42 return caddy.ModuleInfo{
43 ID: "admin.api.pki",
44 New: func() caddy.Module { return new(adminAPI) },
45 }
46 }
47
48 // Provision sets up the adminAPI module.
49 func (a *adminAPI) Provision(ctx caddy.Context) error {
50 a.ctx = ctx
51 a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032)
52
53 // Avoid initializing PKI if it wasn't configured.
54 // We intentionally ignore the error since it's not
55 // fatal if the PKI app is not explicitly configured.
56 pkiApp, err := ctx.AppIfConfigured("pki")
57 if err == nil {
58 a.pkiApp = pkiApp.(*PKI)
59 }
60
61 return nil
62 }
63
64 // Routes returns the admin routes for the PKI app.
65 func (a *adminAPI) Routes() []caddy.AdminRoute {
66 return []caddy.AdminRoute{
67 {
68 Pattern: adminPKIEndpointBase,
69 Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
70 },
71 }
72 }
73
74 // handleAPIEndpoints routes API requests within adminPKIEndpointBase.
75 func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error {
76 uri := strings.TrimPrefix(r.URL.Path, "/pki/")
77 parts := strings.Split(uri, "/")
78 switch {
79 case len(parts) == 2 && parts[0] == "ca" && parts[1] != "":
80 return a.handleCAInfo(w, r)
81 case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates":
82 return a.handleCACerts(w, r)
83 }
84 return caddy.APIError{
85 HTTPStatus: http.StatusNotFound,
86 Err: fmt.Errorf("resource not found: %v", r.URL.Path),
87 }
88 }
89
90 // handleCAInfo returns information about a particular
91 // CA by its ID. If the CA ID is the default, then the CA will be
92 // provisioned if it has not already been. Other CA IDs will return an
93 // error if they have not been previously provisioned.
94 func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error {
95 if r.Method != http.MethodGet {
96 return caddy.APIError{
97 HTTPStatus: http.StatusMethodNotAllowed,
98 Err: fmt.Errorf("method not allowed: %v", r.Method),
99 }
100 }
101
102 ca, err := a.getCAFromAPIRequestPath(r)
103 if err != nil {
104 return err
105 }
106
107 rootCert, interCert, err := rootAndIntermediatePEM(ca)
108 if err != nil {
109 return caddy.APIError{
110 HTTPStatus: http.StatusInternalServerError,
111 Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
112 }
113 }
114
115 repl := ca.newReplacer()
116
117 response := caInfo{
118 ID: ca.ID,
119 Name: ca.Name,
120 RootCN: repl.ReplaceAll(ca.RootCommonName, ""),
121 IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""),
122 RootCert: string(rootCert),
123 IntermediateCert: string(interCert),
124 }
125
126 encoded, err := json.Marshal(response)
127 if err != nil {
128 return caddy.APIError{
129 HTTPStatus: http.StatusInternalServerError,
130 Err: err,
131 }
132 }
133
134 w.Header().Set("Content-Type", "application/json")
135 _, _ = w.Write(encoded)
136
137 return nil
138 }
139
140 // handleCACerts returns the certificate chain for a particular
141 // CA by its ID. If the CA ID is the default, then the CA will be
142 // provisioned if it has not already been. Other CA IDs will return an
143 // error if they have not been previously provisioned.
144 func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
145 if r.Method != http.MethodGet {
146 return caddy.APIError{
147 HTTPStatus: http.StatusMethodNotAllowed,
148 Err: fmt.Errorf("method not allowed: %v", r.Method),
149 }
150 }
151
152 ca, err := a.getCAFromAPIRequestPath(r)
153 if err != nil {
154 return err
155 }
156
157 rootCert, interCert, err := rootAndIntermediatePEM(ca)
158 if err != nil {
159 return caddy.APIError{
160 HTTPStatus: http.StatusInternalServerError,
161 Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
162 }
163 }
164
165 w.Header().Set("Content-Type", "application/pem-certificate-chain")
166 _, err = w.Write(interCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud
167 if err == nil {
168 _, _ = w.Write(rootCert) //nolint:gosec // false positive... no XSS in a PEM for cryin' out loud
169 }
170
171 return nil
172 }
173
174 func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
175 // Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/<ca>)
176 id := strings.Split(r.URL.Path, "/")[3]
177 if id == "" {
178 return nil, caddy.APIError{
179 HTTPStatus: http.StatusBadRequest,
180 Err: fmt.Errorf("missing CA in path"),
181 }
182 }
183
184 // Find the CA by ID, if PKI is configured
185 var ca *CA
186 var ok bool
187 if a.pkiApp != nil {
188 ca, ok = a.pkiApp.CAs[id]
189 }
190
191 // If we didn't find the CA, and PKI is not configured
192 // then we'll either error out if the CA ID is not the
193 // default. If the CA ID is the default, then we'll
194 // provision it, because the user probably aims to
195 // change their config to enable PKI immediately after
196 // if they actually requested the local CA ID.
197 if !ok {
198 if id != DefaultCAID {
199 return nil, caddy.APIError{
200 HTTPStatus: http.StatusNotFound,
201 Err: fmt.Errorf("no certificate authority configured with id: %s", id),
202 }
203 }
204
205 // Provision the default CA, which generates and stores a root
206 // certificate in storage, if one doesn't already exist.
207 ca = new(CA)
208 err := ca.Provision(a.ctx, id, a.log)
209 if err != nil {
210 return nil, caddy.APIError{
211 HTTPStatus: http.StatusInternalServerError,
212 Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
213 }
214 }
215 }
216
217 return ca, nil
218 }
219
220 func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
221 root, err = pemEncodeCert(ca.RootCertificate().Raw)
222 if err != nil {
223 return root, inter, err
224 }
225
226 for _, interCert := range ca.IntermediateCertificateChain() {
227 pemBytes, err := pemEncodeCert(interCert.Raw)
228 if err != nil {
229 return nil, nil, err
230 }
231 inter = append(inter, pemBytes...)
232 }
233
234 return
235 }
236
237 // caInfo is the response structure for the CA info API endpoint.
238 type caInfo struct {
239 ID string `json:"id"`
240 Name string `json:"name"`
241 RootCN string `json:"root_common_name"`
242 IntermediateCN string `json:"intermediate_common_name"`
243 RootCert string `json:"root_certificate"`
244 IntermediateCert string `json:"intermediate_certificate"`
245 }
246
247 // adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist.
248 const adminPKIEndpointBase = "/pki/"
249
250 // Interface guards
251 var (
252 _ caddy.AdminRouter = (*adminAPI)(nil)
253 _ caddy.Provisioner = (*adminAPI)(nil)
254 )