Skip to content
Open
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
87 changes: 65 additions & 22 deletions core/src/main/cfml/context/admin/services.certificates.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
<cflocation url="#request.self#" addtoken="no">
</cfif>



<!---
<!---
Defaults --->
<cfparam name="url.action2" default="list">
<cfparam name="form.mainAction" default="none">
Expand All @@ -24,34 +22,33 @@ Defaults --->
<cfset _port=session.certPort>

<cfscript>
LuceeTrustStore = false;
if ((server.system.properties["lucee.use.lucee.SSL.TrustStore"]?: false)
|| (server.system.environment["lucee_use_lucee_SSL_TrustStore"]?: false)){
LuceeTrustStore = true;
};

customCaCertsEnabled = !(server.system.properties["lucee.ssl.customcacerts.enabled"]?: "true").equalsIgnoreCase("false");
</cfscript>

<cfif !LuceeTrustStore>
<cfif !customCaCertsEnabled>
<p>
<b>As Lucee is currently using the JVM TrustStore/cacerts file, this functionality isn't available.</b>
<b>Custom CA certificates are disabled.</b>
<br><br>
Set the following System or Environment variables to enable: <code>lucee.use.lucee.SSL.TrustStore = true;</code>
Set the following System or Environment variable to enable: <code>lucee.ssl.customcacerts.enabled=true</code>
</p>
</cfif>

<cftry>
<cfswitch expression="#form.mainAction#">
<!--- UPDATE --->

<!--- INSTALL --->
<cfcase value="#stText.services.certificate.install#">
<cfadmin
<cfadmin
type="#request.adminType#"
password="#session["password"&request.adminType]#"
action="updatesslcertificate" host="#form.host#" port="#form.port#">


</cfcase>
</cfcase>
<!--- REMOVE --->
<cfcase value="Remove">
<cfadmin
type="#request.adminType#"
password="#session["password"&request.adminType]#"
action="removesslcertificate" alias="#form.alias#">
</cfcase>
</cfswitch>
<cfcatch>
<cfset error.message=cfcatch.message>
Expand All @@ -61,13 +58,13 @@ Defaults --->
</cftry>


<!---
<!---
Redirtect to entry --->
<cfif cgi.request_method EQ "POST" and error.message EQ "">
<cflocation url="#request.self#?action=#url.action#" addtoken="no">
</cfif>

<!---
<!---
Error Output --->
<cfset printError(error)>
<cfoutput>
Expand Down Expand Up @@ -104,9 +101,55 @@ Error Output --->
</table>
</cfformClassic>

<!--- Installed certificates in custom-cacerts --->
<cfif customCaCertsEnabled>
<cftry>
<cfadmin
type="#request.adminType#"
password="#session["password"&request.adminType]#"
action="getallsslcertificate" returnvariable="installedCerts">

<h2>Installed Certificates</h2>
<cfif installedCerts.recordcount>
<table class="maintbl">
<thead>
<tr>
<th>#stText.services.certificate.subject#</th>
<th>#stText.services.certificate.issuer#</th>
<th>Alias</th>
<th></th>
</tr>
</thead>
<tbody>
<cfloop query="installedCerts">
<tr>
<td>#installedCerts.subject#</td>
<td>#installedCerts.issuer#</td>
<td>#installedCerts.alias#</td>
<td>
<form action="#request.self#?action=#url.action#" method="post" style="display:inline">
<input type="hidden" name="alias" value="#installedCerts.alias#">
<input type="hidden" name="mainAction" value="Remove">
<input class="button small" type="submit" value="Remove" onclick="return confirm('Remove certificate #JSStringFormat(installedCerts.alias)#?')">
</form>
</td>
</tr>
</cfloop>
</tbody>
</table>
<cfelse>
<p>No certificates installed in custom-cacerts.</p>
</cfif>
<cfcatch>
<div class="error">#cfcatch.message# #cfcatch.detail#</div>
</cfcatch>
</cftry>
</cfif>

<!--- Preview certs from remote host --->
<cfif len(_host) and len(_port)>
<cftry>
<cfadmin
<cfadmin
type="#request.adminType#"
password="#session["password"&request.adminType]#"
action="getsslcertificate" host="#_host#" port="#_port#" returnvariable="qry">
Expand Down Expand Up @@ -143,4 +186,4 @@ Error Output --->
</cfcatch>
</cftry>
</cfif>
</cfoutput>
</cfoutput>
18 changes: 12 additions & 6 deletions core/src/main/java/lucee/commons/net/http/HTTPDownloader.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public final class HTTPDownloader {
public static final long DEFAULT_READ_TIMEOUT = 60000; // 60 seconds
private static final String DEFAULT_USER_AGENT = "Lucee";

// Internal downloads talk to a small fixed set of hosts (update.lucee.org, Maven Central)
// and are typically sequential. The pool here is intentionally separate from HTTPEngine4Impl's
// cfhttp pool — see LDEV-5571 plan. User cert installs invalidate cfhttp pools without affecting
// bundle/update downloads, and bundle download lifecycle is decoupled from user request traffic.
private static final int POOL_MAX_CONN = 16;
private static final int POOL_MAX_CONN_PER_ROUTE = 4;

private HTTPDownloader() {
// Utility class, prevent instantiation
}
Expand All @@ -52,7 +59,7 @@ private HTTPDownloader() {
public static void releaseSharedClient() {
synchronized (CLIENT_LOCK) {
if (SHARED_CLIENT != null) {
IOUtil.closeEL(SHARED_CLIENT);
IOUtil.closeEL(SHARED_CLIENT); // managerShared=false → cascades to the owned pool
SHARED_CLIENT = null;
}
}
Expand All @@ -62,8 +69,7 @@ private static CloseableHttpClient getSharedClient() throws GeneralSecurityExcep
if (SHARED_CLIENT == null) {
synchronized (CLIENT_LOCK) {
if (SHARED_CLIENT == null) {
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
SHARED_CLIENT = builder.build();
SHARED_CLIENT = HTTPEngine4Impl.buildUnmanagedClient(null, null, null, null, true, POOL_MAX_CONN_PER_ROUTE, POOL_MAX_CONN, "true");
}
}
}
Expand Down Expand Up @@ -221,7 +227,7 @@ public static InputStream get(URL url, String username, String password, long co

// Handle proxy and credentials
ProxyData proxy = getProxyData(url.getHost());
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");
HttpHost httpHost = new HttpHost(url.getHost(), url.getPort());
HttpContext context = HTTPEngine4Impl.setCredentials(builder, httpHost, username, password, false);
HTTPEngine4Impl.setProxy(url.getHost(), builder, request, proxy);
Expand Down Expand Up @@ -262,7 +268,7 @@ public static HTTPResponse head(URL url, long connectTimeout, long readTimeout,

try {
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");

// Create HTTP HEAD request
HttpHead request = new HttpHead(url.toString());
Expand Down Expand Up @@ -305,7 +311,7 @@ public static boolean exists(URL url) {
public static boolean exists(URL url, long connectTimeout, long readTimeout) {
try {
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");

// Create HTTP HEAD request
HttpHead request = new HttpHead(url.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,13 @@
**/
package lucee.commons.net.http.httpclient;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
Expand All @@ -41,7 +35,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import org.apache.http.Header;
Expand Down Expand Up @@ -73,6 +67,7 @@
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
Expand Down Expand Up @@ -110,6 +105,7 @@
import lucee.runtime.PageContextImpl;
import lucee.runtime.engine.ThreadLocalPageContext;
import lucee.runtime.net.http.ReqRspUtil;
import lucee.runtime.net.http.SSLUtil;
import lucee.runtime.net.http.sni.DefaultHostnameVerifierImpl;
import lucee.runtime.net.http.sni.DefaultHttpClientConnectionOperatorImpl;
import lucee.runtime.net.http.sni.SSLConnectionSocketFactoryImpl;
Expand Down Expand Up @@ -272,10 +268,9 @@ private static Header toHeader(lucee.commons.net.http.Header header) {
return new HeaderImpl(header.getName(), header.getValue());
}

public static HttpClientBuilder getHttpClientBuilder(boolean pooling, String clientCert, String clientCertPassword, String redirect)
throws GeneralSecurityException, IOException {
String key = clientCert + ":" + clientCertPassword;
Registry<ConnectionSocketFactory> reg = StringUtil.isEmpty(clientCert, true) ? createRegistry() : createRegistry(clientCert, clientCertPassword);
public static HttpClientBuilder getHttpClientBuilder(boolean pooling, String clientCert, String clientCertPassword, String trustStore, String trustStorePassword, boolean sslVerify, String redirect) throws GeneralSecurityException {
String key = clientCert + ":" + clientCertPassword + ":" + trustStore + ":" + trustStorePassword + ":" + sslVerify;
Registry<ConnectionSocketFactory> reg = createRegistry( clientCert, clientCertPassword, trustStore, trustStorePassword, sslVerify );

if (!pooling) {
HttpClientBuilder builder = HttpClients.custom();
Expand Down Expand Up @@ -331,31 +326,43 @@ public static void setTimeout(HttpClientBuilder builder, TimeSpan timeout) {
builder.setDefaultRequestConfig(rcBuilder.build());
}

private static Registry<ConnectionSocketFactory> createRegistry() throws GeneralSecurityException {
SSLContext sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, null, new java.security.SecureRandom());
SSLConnectionSocketFactory defaultsslsf = new SSLConnectionSocketFactoryImpl(sslcontext, new DefaultHostnameVerifierImpl());
/* Register connection handlers */
return RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", defaultsslsf).build();
private static Registry<ConnectionSocketFactory> createRegistry( String clientCert, String clientCertPassword, String trustStore, String trustStorePassword, boolean sslVerify ) throws GeneralSecurityException {
SSLContext sslContext;
HostnameVerifier hostnameVerifier;

}
try {
Path clientCertPath = StringUtil.isEmpty( clientCert, true ) ? null : Paths.get( clientCert );
char[] clientPassword = clientCertPassword != null ? clientCertPassword.toCharArray() : null;

if ( !sslVerify ) {
// Disable all SSL verification (like curl -k)
sslContext = SSLUtil.createUnsafeSSLContext( clientCertPath, clientPassword );
hostnameVerifier = NoopHostnameVerifier.INSTANCE;
}
else if ( !StringUtil.isEmpty( trustStore, true ) ) {
// Use custom trust store
Path trustStorePath = Paths.get( trustStore );
char[] trustPassword = trustStorePassword != null ? trustStorePassword.toCharArray() : "changeit".toCharArray();
List<SSLUtil.TrustStoreConfig> additionalTrustStores = new ArrayList<>();
additionalTrustStores.add( new SSLUtil.TrustStoreConfig( trustStorePath, trustPassword ) );
sslContext = SSLUtil.createSSLContext( clientCertPath, clientPassword, additionalTrustStores );
hostnameVerifier = new DefaultHostnameVerifierImpl();
}
else {
// Standard mode with JVM + custom-cacerts
sslContext = SSLUtil.createSSLContext( clientCertPath, clientPassword );
hostnameVerifier = new DefaultHostnameVerifierImpl();
}
}
catch ( IOException e ) {
throw new GeneralSecurityException( "Failed to create SSL context", e );
}

private static Registry<ConnectionSocketFactory> createRegistry(String clientCert, String clientCertPassword)
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
// Currently, clientCert force usePool to being ignored
if (clientCertPassword == null) clientCertPassword = "";
// Load the client cert
File ksFile = new File(clientCert);
KeyStore clientStore = KeyStore.getInstance("PKCS12");
clientStore.load(new FileInputStream(ksFile), clientCertPassword.toCharArray());
// Prepare the keys
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientStore, clientCertPassword.toCharArray());
SSLContext sslcontext = SSLContext.getInstance("TLS");
// Configure the socket factory
sslcontext.init(kmf.getKeyManagers(), null, new java.security.SecureRandom());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactoryImpl(sslcontext, new DefaultHostnameVerifierImpl());
return RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslsf).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactoryImpl( sslContext, hostnameVerifier );
return RegistryBuilder.<ConnectionSocketFactory>create()
.register( "http", PlainConnectionSocketFactory.getSocketFactory() )
.register( "https", sslsf )
.build();
}

public static void releaseConnectionManager() {
Expand All @@ -366,6 +373,29 @@ public static void releaseConnectionManager() {
}
}

/**
* Builds a CloseableHttpClient backed by a fresh connection pool that is NOT registered in
* the shared connectionManagers map. The client owns the pool (managerShared=false), so
* closing the client closes the pool. Use for internal infrastructure traffic whose lifecycle
* must be independent of user-driven releaseConnectionManager() calls.
*/
public static CloseableHttpClient buildUnmanagedClient(String clientCert, String clientCertPassword, String trustStore, String trustStorePassword,
boolean sslVerify, int maxPerRoute, int maxTotal, String redirect) throws GeneralSecurityException {
Registry<ConnectionSocketFactory> reg = createRegistry(clientCert, clientCertPassword, trustStore, trustStorePassword, sslVerify);
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(new DefaultHttpClientConnectionOperatorImpl(reg), null, POOL_CONN_TTL_MS,
TimeUnit.MILLISECONDS);
cm.setDefaultMaxPerRoute(maxPerRoute);
cm.setMaxTotal(maxTotal);
cm.setDefaultSocketConfig(SocketConfig.copy(SocketConfig.DEFAULT).setTcpNoDelay(true).setSoReuseAddress(true).setSoLinger(0).build());

HttpClientBuilder builder = HttpClients.custom().setConnectionManager(cm).setConnectionManagerShared(false)
.setConnectionTimeToLive(POOL_CONN_TTL_MS, TimeUnit.MILLISECONDS).setConnectionReuseStrategy(new DefaultClientConnectionReuseStrategy())
.setRedirectStrategy("lax".equalsIgnoreCase(redirect) ? new LaxRedirectStrategy() : new DefaultRedirectStrategy())
.setRetryHandler(new NoHttpResponseExceptionHttpRequestRetryHandler());
if (!Caster.toBooleanValue(redirect, true)) builder.disableRedirectHandling();
return builder.build();
}

public static boolean isShutDown(PoolingHttpClientConnectionManager cm, boolean defaultValue) {
if (cm != null && !cannotAccess) {
try {
Expand Down Expand Up @@ -399,7 +429,7 @@ private static HTTPResponse invoke(URL url, HttpUriRequest request, String usern
CloseableHttpClient client;
proxy = ProxyDataImpl.validate(proxy, url.getHost());

HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, String.valueOf(redirect));
HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, null, null, true, String.valueOf(redirect));

HttpHost hh = new HttpHost(url.getHost(), url.getPort());
setHeader(request, headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
import java.util.ArrayList;
Expand Down Expand Up @@ -157,6 +158,7 @@
import lucee.runtime.monitor.RequestMonitorProImpl;
import lucee.runtime.monitor.RequestMonitorWrap;
import lucee.runtime.net.http.ReqRspUtil;
import lucee.runtime.net.http.SSLUtil;
import lucee.runtime.net.mail.Server;
import lucee.runtime.net.mail.ServerImpl;
import lucee.runtime.net.proxy.ProxyData;
Expand Down Expand Up @@ -338,6 +340,7 @@ public static ConfigServerImpl newInstanceServer(CFMLEngineImpl engine, Map<Stri
config.setRoot(root);
// admin mode
load(config, root, false, doNew, essentialOnly);
SSLUtil.init( Paths.get( config.getConfigDir().getAbsolutePath(), "security" ) );

if (!essentialOnly) {
createContextFiles(configDir, config, doNew);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1359,7 +1359,7 @@ public void reset(String configId) {
Controler cntr = getControler();
if (cntr != null) cntr.close();

// release HTTP Pool
// release HTTP Pools — cfhttp's shared pools and HTTPDownloader's separate internal pool
HTTPEngine4Impl.releaseConnectionManager();
HTTPDownloader.releaseSharedClient();

Expand Down
Loading
Loading