Skip to content

Commit

Permalink
feat(server): add DELETE API for Carapace certificates
Browse files Browse the repository at this point in the history
Closes #454
  • Loading branch information
NiccoMlt committed Jun 6, 2024
1 parent bfc2be0 commit 64b301a
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import lombok.AllArgsConstructor;
Expand All @@ -67,6 +70,8 @@
import org.carapaceproxy.core.RuntimeServerConfiguration;
import org.carapaceproxy.server.certificates.DynamicCertificateState;
import org.carapaceproxy.server.certificates.DynamicCertificatesManager;
import org.carapaceproxy.server.config.ConfigurationChangeInProgressException;
import org.carapaceproxy.server.config.ConfigurationNotValidException;
import org.carapaceproxy.server.config.SSLCertificateConfiguration;
import org.carapaceproxy.server.config.SSLCertificateConfiguration.CertificateMode;
import org.carapaceproxy.utils.CertificatesUtils;
Expand All @@ -80,11 +85,10 @@
@Produces(MediaType.APPLICATION_JSON)
public class CertificatesResource {

public static final Set<DynamicCertificateState> AVAILABLE_CERTIFICATES_STATES_FOR_UPLOAD = Set.of(AVAILABLE, WAITING);
private static final Logger LOG = Logger.getLogger(CertificatesResource.class.getName());

@javax.ws.rs.core.Context
ServletContext context;
@Context
private ServletContext context;

@Data
@AllArgsConstructor
Expand All @@ -93,7 +97,7 @@ public static final class CertificatesResponse {
private final Collection<CertificateBean> certificates;
private final String localStorePath;

public CertificatesResponse(Collection<CertificateBean> certificates, HttpProxyServer server) {
public CertificatesResponse(final Collection<CertificateBean> certificates, final HttpProxyServer server) {
this.certificates = certificates;
this.localStorePath = server.getCurrentConfiguration().getLocalCertificatesStorePath();
}
Expand Down Expand Up @@ -165,7 +169,11 @@ public CertificatesResponse getAllCertificates() {
return new CertificatesResponse(res.values(), server);
}

private static void fillCertificateBean(CertificateBean bean, SSLCertificateConfiguration certificate, DynamicCertificatesManager dCManager, HttpProxyServer server) {
private static void fillCertificateBean(
final CertificateBean bean,
final SSLCertificateConfiguration certificate,
final DynamicCertificatesManager dCManager,
final HttpProxyServer server) {
try {
DynamicCertificateState state = null;
if (certificate.isDynamic()) {
Expand Down Expand Up @@ -208,7 +216,7 @@ private static void fillCertificateBean(CertificateBean bean, SSLCertificateConf

@GET
@Path("{certId}")
public CertificatesResponse getCertificateById(@PathParam("certId") String certId) {
public CertificatesResponse getCertificateById(@PathParam("certId") final String certId) {
final var cert = findCertificateById(certId);
return new CertificatesResponse(
cert != null ? List.of(cert) : Collections.emptyList(),
Expand Down Expand Up @@ -261,25 +269,40 @@ public Response createCertificate(CertificateForm form) {
return FormValidationResponse.created();
}

@DELETE
@Path("{certId}")
public Response deleteCertificate(@PathParam("certId") final String certId) {
final var server = (HttpProxyServer) context.getAttribute("server");
final var certificates = server.getCurrentConfiguration().getCertificates();
if (!certificates.containsKey(certId)) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
try {
server.rewriteConfiguration(it -> it.removeCertificate(certId));
return SimpleResponse.ok();
} catch (ConfigurationChangeInProgressException | InterruptedException | ConfigurationNotValidException e) {
return SimpleResponse.error(e);
}
}

@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Path("{certId}/download")
public Response downloadCertificateById(@PathParam("certId") String certId) throws GeneralSecurityException {
public Response downloadCertificateById(@PathParam("certId") final String certId) {
CertificateBean cert = findCertificateById(certId);
byte[] data = new byte[0];
if (cert != null && cert.isDynamic()) {
HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");
DynamicCertificatesManager dynamicCertificateManager = server.getDynamicCertificatesManager();
data = dynamicCertificateManager.getCertificateForDomain(cert.getId());
if (cert == null || !cert.isDynamic()) {
return Response.status(Response.Status.NOT_FOUND).build();
}

HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");
DynamicCertificatesManager dynamicCertificateManager = server.getDynamicCertificatesManager();
final var data = dynamicCertificateManager.getCertificateForDomain(cert.getId());
return Response
.ok(data, MediaType.APPLICATION_OCTET_STREAM)
.header("content-disposition", "attachment; filename = " + cert.getId() + ".p12")
.build();
}

private CertificateBean findCertificateById(String certId) {
private CertificateBean findCertificateById(final String certId) {
HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");
SSLCertificateConfiguration certificate = server.getCurrentConfiguration().getCertificates().get(certId);
DynamicCertificatesManager dCManager = server.getDynamicCertificatesManager();
Expand All @@ -295,19 +318,18 @@ private CertificateBean findCertificateById(String certId) {
fillCertificateBean(certBean, certificate, dCManager, server);
return certBean;
}

return null;
}

@POST
@Path("{domain}/upload")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
public Response uploadCertificate(
@PathParam("domain") String domain,
@QueryParam("subjectaltnames") List<String> subjectAltNames,
@QueryParam("type") @DefaultValue("manual") String type,
@QueryParam("daysbeforerenewal") Integer daysbeforerenewal,
InputStream uploadedInputStream) throws Exception {
@PathParam("domain") final String domain,
@QueryParam("subjectaltnames") final List<String> subjectAltNames,
@QueryParam("type") @DefaultValue("manual") final String type,
@QueryParam("daysbeforerenewal") final Integer daysbeforerenewal,
final InputStream uploadedInputStream) throws Exception {

try (InputStream input = uploadedInputStream) {
// Certificate type (manual | acme)
Expand Down Expand Up @@ -354,26 +376,25 @@ public Response uploadCertificate(

@POST
@Path("{domain}/store")
public Response storeLocalCertificate(@PathParam("domain") String domain) throws Exception {
public Response storeLocalCertificate(@PathParam("domain") final String domain) {
var server = ((HttpProxyServer) context.getAttribute("server"));
server.getDynamicCertificatesManager().forceStoreLocalCertificates(domain);
return SimpleResponse.ok();
}

@POST
@Path("/storeall")
public Response storeAllCertificates() throws Exception {
public Response storeAllCertificates() {
var server = ((HttpProxyServer) context.getAttribute("server"));
server.getDynamicCertificatesManager().forceStoreLocalCertificates();
return SimpleResponse.ok();
}

@POST
@Path("{domain}/reset")
public Response resetCertificateState(@PathParam("domain") String domain) throws Exception {
public Response resetCertificateState(@PathParam("domain") final String domain) {
var server = ((HttpProxyServer) context.getAttribute("server"));
server.getDynamicCertificatesManager().setStateOfCertificate(domain, WAITING);
return SimpleResponse.ok();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ default void commitConfiguration(ConfigurationStore newConfigurationStore) {

void saveCertificate(CertificateData cert);

void removeCertificate(String certId);

void reload();

void saveAcmeChallengeToken(String id, String data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,63 +70,82 @@ public class HerdDBConfigurationStore implements ConfigurationStore {

// Main table
private static final String CONFIG_TABLE_NAME = "proxy_config";
private static final String CREATE_CONFIG_TABLE = "CREATE TABLE " + CONFIG_TABLE_NAME + "(pname string primary key, pvalue string)";
private static final String SELECT_ALL_FROM_CONFIG_TABLE = "SELECT pname,pvalue from " + CONFIG_TABLE_NAME;
private static final String UPDATE_CONFIG_TABLE = "UPDATE " + CONFIG_TABLE_NAME + " set pvalue=? WHERE pname=?";
private static final String DELETE_FROM_CONFIG_TABLE = "DELETE FROM " + CONFIG_TABLE_NAME + " WHERE pname=?";
private static final String INSERT_INTO_CONFIG_TABLE = "INSERT INTO " + CONFIG_TABLE_NAME + "(pname,pvalue) values (?,?)";
private static final String CREATE_CONFIG_TABLE = """
CREATE TABLE %s(pname string primary key, pvalue string)
""".formatted(CONFIG_TABLE_NAME);
private static final String SELECT_ALL_FROM_CONFIG_TABLE = """
SELECT pname, pvalue from %s
""".formatted(CONFIG_TABLE_NAME);
private static final String UPDATE_CONFIG_TABLE = """
UPDATE %s set pvalue=? WHERE pname=?
""".formatted(CONFIG_TABLE_NAME);
private static final String DELETE_FROM_CONFIG_TABLE = """
DELETE FROM %s WHERE pname=?
""".formatted(CONFIG_TABLE_NAME);
private static final String INSERT_INTO_CONFIG_TABLE = """
INSERT INTO %s(pname, pvalue) values (?, ?)
""".formatted(CONFIG_TABLE_NAME);

// Table for KeyPairs
private static final String KEYPAIR_TABLE_NAME = "keypairs";
private static final String CREATE_KEYPAIR_TABLE = "CREATE TABLE " + KEYPAIR_TABLE_NAME
+ "(domain string primary key, privateKey string, publicKey string)";
private static final String SELECT_FROM_KEYPAIR_TABLE = "SELECT privateKey, publicKey FROM " + KEYPAIR_TABLE_NAME
+ " WHERE domain=?";
private static final String UPDATE_KEYPAIR_TABLE = "UPDATE " + KEYPAIR_TABLE_NAME
+ " SET privateKey=?, publicKey=? WHERE domain=?";
private static final String INSERT_INTO_KEYPAIR_TABLE = "INSERT INTO " + KEYPAIR_TABLE_NAME
+ "(domain, privateKey, publicKey) values (?, ?, ?)";
private static final String CREATE_KEYPAIR_TABLE = """
CREATE TABLE %s(domain string primary key, privateKey string, publicKey string)
""".formatted(KEYPAIR_TABLE_NAME);
private static final String SELECT_FROM_KEYPAIR_TABLE = """
SELECT privateKey, publicKey FROM %s WHERE domain=?
""".formatted(KEYPAIR_TABLE_NAME);
private static final String UPDATE_KEYPAIR_TABLE = """
UPDATE %s SET privateKey=?, publicKey=? WHERE domain=?
""".formatted(KEYPAIR_TABLE_NAME);
private static final String INSERT_INTO_KEYPAIR_TABLE = """
INSERT INTO %s(domain, privateKey, publicKey) values (?, ?, ?)
""".formatted(KEYPAIR_TABLE_NAME);

// Table for ACME Certificates
private static final String DIGITAL_CERTIFICATES_TABLE_NAME = "digital_certificates";
private static final String CREATE_DIGITAL_CERTIFICATES_TABLE = """
CREATE TABLE %s (
domain string primary key,
subjectAltNames string,
chain string,
state string,
pendingOrder string,
pendingChallenges string,
attemptCount int,
message string
)""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);

CREATE TABLE %s (
domain string primary key,
subjectAltNames string,
chain string,
state string,
pendingOrder string,
pendingChallenges string,
attemptCount int,
message string
)""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);
private static final String SELECT_FROM_DIGITAL_CERTIFICATES_TABLE = """
SELECT domain, subjectAltNames, chain, state, pendingOrder, pendingChallenges, attemptCount, message
FROM %s
WHERE domain=?
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);

SELECT domain, subjectAltNames, chain, state, pendingOrder, pendingChallenges, attemptCount, message
FROM %s
WHERE domain=?
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);
private static final String UPDATE_DIGITAL_CERTIFICATES_TABLE = """
UPDATE %s
SET subjectAltNames=?, chain=?, state=?, pendingOrder=?, pendingChallenges=?, attemptCount=?, message=?
WHERE domain=?
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);

UPDATE %s
SET subjectAltNames=?, chain=?, state=?, pendingOrder=?, pendingChallenges=?, attemptCount=?, message=?
WHERE domain=?
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);
private static final String INSERT_INTO_DIGITAL_CERTIFICATES_TABLE = """
INSERT INTO %s(domain, subjectAltNames, chain, state, pendingOrder, pendingChallenges, attemptCount, message)
values (?, ?, ?, ?, ?, ?, ?, ?)
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);
INSERT INTO %s(domain, subjectAltNames, chain, state, pendingOrder, pendingChallenges, attemptCount, message)
values (?, ?, ?, ?, ?, ?, ?, ?)
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);
private static final String REMOVE_DIGITAL_CERTIFICATES_TABLE = """
DELETE FROM %s WHERE domain=?
""".formatted(DIGITAL_CERTIFICATES_TABLE_NAME);

// Table for ACME challenge tokens
private static final String ACME_CHALLENGE_TOKENS_TABLE_NAME = "acme_challenge_tokens";
private static final String CREATE_ACME_CHALLENGE_TOKENS_TABLE = "CREATE TABLE " + ACME_CHALLENGE_TOKENS_TABLE_NAME
+ "(id string primary key, data string)";
private static final String SELECT_FROM_ACME_CHALLENGE_TOKENS_TABLE = "SELECT data from " + ACME_CHALLENGE_TOKENS_TABLE_NAME + " WHERE id=?";
private static final String INSERT_INTO_ACME_CHALLENGE_TOKENS_TABLE = "INSERT INTO " + ACME_CHALLENGE_TOKENS_TABLE_NAME
+ "(id, data) values (?, ?)";
private static final String DELETE_FROM_ACME_CHALLENGE_TOKENS_TABLE = "DELETE from " + ACME_CHALLENGE_TOKENS_TABLE_NAME
+ " WHERE id=?";
private static final String CREATE_ACME_CHALLENGE_TOKENS_TABLE = """
CREATE TABLE %s(id string primary key, data string)
""".formatted(ACME_CHALLENGE_TOKENS_TABLE_NAME);
private static final String SELECT_FROM_ACME_CHALLENGE_TOKENS_TABLE = """
SELECT data from %s WHERE id=?
""".formatted(ACME_CHALLENGE_TOKENS_TABLE_NAME);
private static final String INSERT_INTO_ACME_CHALLENGE_TOKENS_TABLE = """
INSERT INTO %s(id, data) values (?, ?)
""".formatted(ACME_CHALLENGE_TOKENS_TABLE_NAME);
private static final String DELETE_FROM_ACME_CHALLENGE_TOKENS_TABLE = """
DELETE from %s WHERE id=?
""".formatted(ACME_CHALLENGE_TOKENS_TABLE_NAME);

private static final Logger LOG = Logger.getLogger(HerdDBConfigurationStore.class.getName());

Expand Down Expand Up @@ -475,6 +494,18 @@ public void saveCertificate(CertificateData cert) {
}
}

@Override
public void removeCertificate(final String certId) {
try (final var connection = datasource.getConnection();
final var preparedStatement = connection.prepareStatement(REMOVE_DIGITAL_CERTIFICATES_TABLE)) {
preparedStatement.setString(1, certId);
preparedStatement.executeUpdate();
} catch (final SQLException err) {
LOG.log(Level.SEVERE, "Error while performing Certificate drop for domain " + certId + ".", err);
throw new ConfigurationStoreException(err);
}
}

private static String formatChallengesData(Map<String, JSON> challengesData) throws JsonProcessingException {
if (challengesData == null || challengesData.isEmpty()) {
return null;
Expand Down
Loading

0 comments on commit 64b301a

Please sign in to comment.