diff --git a/carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java b/carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java index 04ade8977..cc7389e47 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java +++ b/carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java @@ -48,6 +48,7 @@ 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; @@ -55,6 +56,8 @@ 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; @@ -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; @@ -80,11 +85,10 @@ @Produces(MediaType.APPLICATION_JSON) public class CertificatesResource { - public static final Set 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 @@ -93,7 +97,7 @@ public static final class CertificatesResponse { private final Collection certificates; private final String localStorePath; - public CertificatesResponse(Collection certificates, HttpProxyServer server) { + public CertificatesResponse(final Collection certificates, final HttpProxyServer server) { this.certificates = certificates; this.localStorePath = server.getCurrentConfiguration().getLocalCertificatesStorePath(); } @@ -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()) { @@ -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(), @@ -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(); @@ -295,7 +318,6 @@ private CertificateBean findCertificateById(String certId) { fillCertificateBean(certBean, certificate, dCManager, server); return certBean; } - return null; } @@ -303,11 +325,11 @@ private CertificateBean findCertificateById(String certId) { @Path("{domain}/upload") @Consumes(MediaType.APPLICATION_OCTET_STREAM) public Response uploadCertificate( - @PathParam("domain") String domain, - @QueryParam("subjectaltnames") List subjectAltNames, - @QueryParam("type") @DefaultValue("manual") String type, - @QueryParam("daysbeforerenewal") Integer daysbeforerenewal, - InputStream uploadedInputStream) throws Exception { + @PathParam("domain") final String domain, + @QueryParam("subjectaltnames") final List 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) @@ -354,7 +376,7 @@ 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(); @@ -362,7 +384,7 @@ public Response storeLocalCertificate(@PathParam("domain") String domain) throws @POST @Path("/storeall") - public Response storeAllCertificates() throws Exception { + public Response storeAllCertificates() { var server = ((HttpProxyServer) context.getAttribute("server")); server.getDynamicCertificatesManager().forceStoreLocalCertificates(); return SimpleResponse.ok(); @@ -370,10 +392,9 @@ public Response storeAllCertificates() throws Exception { @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(); } - } diff --git a/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java b/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java index 953f839ce..b99939916 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java +++ b/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java @@ -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); diff --git a/carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java b/carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java index 895921e0c..26bacdf07 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java +++ b/carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java @@ -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()); @@ -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 challengesData) throws JsonProcessingException { if (challengesData == null || challengesData.isEmpty()) { return null; diff --git a/carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java b/carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java index d37aa0481..7063cd439 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java +++ b/carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java @@ -20,9 +20,14 @@ package org.carapaceproxy.configstore; import java.security.KeyPair; +import java.util.Optional; import java.util.Properties; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.carapaceproxy.server.config.ConnectionPoolConfiguration; /** @@ -99,6 +104,50 @@ public void saveCertificate(CertificateData cert) { certificates.put(cert.getDomain(), cert); } + @Override + public void removeCertificate(final String certId) { + this.certificates.remove(certId); + this.removePropertiesAtId("certificate", certId); + } + + private void removePropertiesAtId(final String prefix, final String id) { + findPropertyPrefix(prefix, id).ifPresent(indexedPrefix -> { + final var iterator = properties.propertyNames().asIterator(); + final var spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); + final var propertyNames = StreamSupport + .stream(spliterator, false) + .filter(String.class::isInstance) + .map(String.class::cast) + .filter(it -> it.startsWith(indexedPrefix)) + .collect(Collectors.toUnmodifiableSet()); + for (final var propertyName : propertyNames) { + properties.remove(propertyName); + } + }); + } + + /** + * Search for a property with the format {@code ..id} that matches the provided ID. + * If found, it is returned to the caller; if not, an empty option will be the result. + *
+ * Please note that if many IDs are present that would match, only the first will be returned, + * as there should have been only one anyway... + * + * @param prefix the prefix to look for + * @param id the ID to look for inside the prefix + * @return the result of the search + */ + private Optional findPropertyPrefix(final String prefix, final String id) { + final var max = findMaxIndexForPrefix(prefix); + for (int index = 0; index <= max; index++) { + String indexedPrefix = prefix + "." + index + "."; + if (id.equals(properties.getProperty(indexedPrefix + "id"))) { + return Optional.of(indexedPrefix); + } + } + return Optional.empty(); + } + @Override public void reload() { } @@ -122,18 +171,6 @@ public void addConnectionPool(final ConnectionPoolConfiguration connectionPool) saveConnectionPool(connectionPool, findMaxIndexForPrefix("connectionpool") + 1); } - public void updateConnectionPool(final ConnectionPoolConfiguration connectionPool) { - final var max = findMaxIndexForPrefix("connectionpool"); - for (int index = 0; index <= max; index++) { - final var prefix = "connectionpool." + index + "."; - final var id = properties.getProperty(prefix + "id", null); - if (connectionPool.getId().equals(id)) { - saveConnectionPool(connectionPool, index); - return; - } - } - } - private void saveConnectionPool(final ConnectionPoolConfiguration connectionPool, final int index) { final var prefix = "connectionpool." + index + "."; properties.setProperty(prefix + "id", connectionPool.getId()); @@ -152,27 +189,19 @@ private void saveConnectionPool(final ConnectionPoolConfiguration connectionPool properties.setProperty(prefix + "keepalive", String.valueOf(connectionPool.isKeepAlive())); } - public void deleteConnectionPool(final String id) { + public void updateConnectionPool(final ConnectionPoolConfiguration connectionPool) { final var max = findMaxIndexForPrefix("connectionpool"); for (int index = 0; index <= max; index++) { - String prefix = "connectionpool." + index + "."; - if (id.equals(properties.getProperty(prefix + "id"))) { - properties.remove(prefix + "id"); - properties.remove(prefix + "domain"); - properties.remove(prefix + "maxconnectionsperendpoint"); - properties.remove(prefix + "borrowtimeout"); - properties.remove(prefix + "connecttimeout"); - properties.remove(prefix + "stuckrequesttimeout"); - properties.remove(prefix + "idletimeout"); - properties.remove(prefix + "maxlifetime"); - properties.remove(prefix + "disposetimeout"); - properties.remove(prefix + "keepaliveidle"); - properties.remove(prefix + "keepaliveinterval"); - properties.remove(prefix + "keepalivecount"); - properties.remove(prefix + "enabled"); - properties.remove(prefix + "keepalive"); + final var prefix = "connectionpool." + index + "."; + final var id = properties.getProperty(prefix + "id", null); + if (connectionPool.getId().equals(id)) { + saveConnectionPool(connectionPool, index); return; } } } + + public void deleteConnectionPool(final String id) { + this.removePropertiesAtId("connectionpool", id); + } } diff --git a/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java b/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java index 4451c5929..fc18da5cd 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java @@ -906,6 +906,9 @@ public void testCreateCertificateFromUI() throws Exception { result = resp.getData(FormValidationResponse.class); assertEquals("domain", result.getField()); assertEquals(FormValidationResponse.ERROR_FIELD_DUPLICATED, result.getMessage()); + + resp = client.delete("/api/certificates/test.domain.tld", credentials); + assertTrue(resp.isOk()); } } @@ -917,5 +920,4 @@ private static Set getCertificateIndicesWithHostname(final Configuratio .map(entry -> Integer.parseInt(entry.getKey().split("\\.")[0])) .collect(Collectors.toUnmodifiableSet()); } - }