caddy/modules/caddypki/adminapi.go
Matthew Holt e43b6d8178 core: Variadic Context.Logger(); soft deprecation
Ideally I'd just remove the parameter to caddy.Context.Logger(), but
this would break most Caddy plugins.

Instead, I'm making it variadic and marking it as partially deprecated.
In the future, I might completely remove the parameter once most
plugins have updated.
2022-09-16 16:55:36 -06:00

256 lines
7.1 KiB
Go

// Copyright 2020 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 caddypki
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(adminAPI{})
}
// adminAPI is a module that serves PKI endpoints to retrieve
// information about the CAs being managed by Caddy.
type adminAPI struct {
ctx caddy.Context
log *zap.Logger
pkiApp *PKI
}
// CaddyModule returns the Caddy module information.
func (adminAPI) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "admin.api.pki",
New: func() caddy.Module { return new(adminAPI) },
}
}
// Provision sets up the adminAPI module.
func (a *adminAPI) Provision(ctx caddy.Context) error {
a.ctx = ctx
a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032)
// First check if the PKI app was configured, because
// a.ctx.App() has the side effect of instantiating
// and provisioning an app even if it wasn't configured.
pkiAppConfigured := a.ctx.AppIsConfigured("pki")
if !pkiAppConfigured {
return nil
}
// Load the PKI app, so we can query it for information.
appModule, err := a.ctx.App("pki")
if err != nil {
return err
}
a.pkiApp = appModule.(*PKI)
return nil
}
// Routes returns the admin routes for the PKI app.
func (a *adminAPI) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{
{
Pattern: adminPKIEndpointBase,
Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
},
}
}
// handleAPIEndpoints routes API requests within adminPKIEndpointBase.
func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error {
uri := strings.TrimPrefix(r.URL.Path, "/pki/")
parts := strings.Split(uri, "/")
switch {
case len(parts) == 2 && parts[0] == "ca" && parts[1] != "":
return a.handleCAInfo(w, r)
case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates":
return a.handleCACerts(w, r)
}
return caddy.APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("resource not found: %v", r.URL.Path),
}
}
// handleCAInfo returns information about a particular
// CA by its ID. If the CA ID is the default, then the CA will be
// provisioned if it has not already been. Other CA IDs will return an
// error if they have not been previously provisioned.
func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodGet {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed: %v", r.Method),
}
}
ca, err := a.getCAFromAPIRequestPath(r)
if err != nil {
return err
}
rootCert, interCert, err := rootAndIntermediatePEM(ca)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
}
}
repl := ca.newReplacer()
response := caInfo{
ID: ca.ID,
Name: ca.Name,
RootCN: repl.ReplaceAll(ca.RootCommonName, ""),
IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""),
RootCert: string(rootCert),
IntermediateCert: string(interCert),
}
encoded, err := json.Marshal(response)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: err,
}
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(encoded)
return nil
}
// handleCACerts returns the certificate chain for a particular
// CA by its ID. If the CA ID is the default, then the CA will be
// provisioned if it has not already been. Other CA IDs will return an
// error if they have not been previously provisioned.
func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodGet {
return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed: %v", r.Method),
}
}
ca, err := a.getCAFromAPIRequestPath(r)
if err != nil {
return err
}
rootCert, interCert, err := rootAndIntermediatePEM(ca)
if err != nil {
return caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
}
}
w.Header().Set("Content-Type", "application/pem-certificate-chain")
_, err = w.Write(interCert)
if err == nil {
_, _ = w.Write(rootCert)
}
return nil
}
func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
// Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/<ca>)
id := strings.Split(r.URL.Path, "/")[3]
if id == "" {
return nil, caddy.APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("missing CA in path"),
}
}
// Find the CA by ID, if PKI is configured
var ca *CA
var ok bool
if a.pkiApp != nil {
ca, ok = a.pkiApp.CAs[id]
}
// If we didn't find the CA, and PKI is not configured
// then we'll either error out if the CA ID is not the
// default. If the CA ID is the default, then we'll
// provision it, because the user probably aims to
// change their config to enable PKI immediately after
// if they actually requested the local CA ID.
if !ok {
if id != DefaultCAID {
return nil, caddy.APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("no certificate authority configured with id: %s", id),
}
}
// Provision the default CA, which generates and stores a root
// certificate in storage, if one doesn't already exist.
ca = new(CA)
err := ca.Provision(a.ctx, id, a.log)
if err != nil {
return nil, caddy.APIError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
}
}
}
return ca, nil
}
func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
root, err = pemEncodeCert(ca.RootCertificate().Raw)
if err != nil {
return
}
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
if err != nil {
return
}
return
}
// caInfo is the response structure for the CA info API endpoint.
type caInfo struct {
ID string `json:"id"`
Name string `json:"name"`
RootCN string `json:"root_common_name"`
IntermediateCN string `json:"intermediate_common_name"`
RootCert string `json:"root_certificate"`
IntermediateCert string `json:"intermediate_certificate"`
}
// adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist.
const adminPKIEndpointBase = "/pki/"
// Interface guards
var (
_ caddy.AdminRouter = (*adminAPI)(nil)
_ caddy.Provisioner = (*adminAPI)(nil)
)