Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): add DELETE API for Carapace certificates #475

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()) {
dmercuriali marked this conversation as resolved.
Show resolved Hide resolved
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