ConnettoreHTTPCOREConnectionManager.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2025 Link.it srl (https://link.it). 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package org.openspcoop2.pdd.core.connettori.httpcore5;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

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

import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.openspcoop2.pdd.config.OpenSPCoop2Properties;
import org.openspcoop2.pdd.core.connettori.AbstractConnettoreConnectionConfig;
import org.openspcoop2.pdd.core.connettori.ConnettoreException;
import org.openspcoop2.pdd.core.connettori.ConnettoreHttpPoolParams;
import org.openspcoop2.pdd.core.connettori.ConnettoreLogger;
import org.openspcoop2.utils.SemaphoreLock;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.UtilsMultiException;
import org.openspcoop2.utils.resources.Loader;
import org.openspcoop2.utils.transport.http.SSLUtilities;
import org.openspcoop2.utils.transport.http.WrappedLogTlsSocketStrategy;

/**
 * ConnettoreHTTPCOREConnectionManager
 *
 *
 * @author Poli Andrea (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class ConnettoreHTTPCOREConnectionManager {

	private ConnettoreHTTPCOREConnectionManager() {}
	
	// ******** STATIC **********

	private static final org.openspcoop2.utils.Semaphore semaphorePoolingConnectionManager = new org.openspcoop2.utils.Semaphore(ConnettoreHTTPCORE.ID_HTTPCORE+"-PoolingConnectionManager"); // usato per instanziare un pool
	
	public static final boolean USE_POOL_CONNECTION = false; // ha senso solo con il NIO dove si utilizza una sola connessione
	private static final org.openspcoop2.utils.Semaphore semaphoreConnection = new org.openspcoop2.utils.Semaphore(ConnettoreHTTPCORE.ID_HTTPCORE+"-ConnectionManager"); // usato per negoziare una connessione da un pool
	
	static Map<String, PoolingHttpClientConnectionManager> mapPoolingConnectionManager = new HashMap<>();
	static Map<String, ConnettoreHTTPCOREConnection> mapConnection = new HashMap<>();
	
	private static ConnettoreHTTPCOREConnectionEvictor idleConnectionEvictor;
		
	public static synchronized void initialize() throws ConnettoreException {
		try {
			if(ConnettoreHTTPCOREConnectionManager.idleConnectionEvictor==null) {
				OpenSPCoop2Properties op2Properties = OpenSPCoop2Properties.getInstance();
				Integer closeIdleConnectionsAfterSeconds = op2Properties.getBIOConfigSyncClientCloseIdleConnectionsAfterSeconds();
				boolean idleConnectionEvictorEnabled = (closeIdleConnectionsAfterSeconds!=null && closeIdleConnectionsAfterSeconds>0);
				if(idleConnectionEvictorEnabled) {
					int sleepTimeSeconds = op2Properties.getBIOConfigSyncClientCloseIdleConnectionsCheckIntervalSeconds();
					boolean debug = op2Properties.isBIOConfigSyncClientCloseIdleConnectionsDebug();
					ConnettoreHTTPCOREConnectionManager.idleConnectionEvictor = 
							new ConnettoreHTTPCOREConnectionEvictor(debug, sleepTimeSeconds, closeIdleConnectionsAfterSeconds);
					idleConnectionEvictor.start();
				}
			}
		}catch(Exception t) {
			throw new ConnettoreException(t.getMessage(),t);
		}
	}
	
	public static String getHttpClientConnectionManagerStatus() {
		if(idleConnectionEvictor!=null) {
			StringBuilder sb = new StringBuilder();
			idleConnectionEvictor.internalCheck(sb);
			return sb.toString();
		}
		return null;
	}
	
	private static void initialize(String key, TlsSocketStrategy tlsSocketStrategy, 
			ConnettoreHTTPCOREConnectionConfig connectionConfig, ConnettoreLogger logger) throws ConnettoreException {
		
		String idTransazione = logger!=null ? logger.getIdTransazione() : null;
		SemaphoreLock lock = null;
		try {
			lock = semaphorePoolingConnectionManager.acquire("initPoolingConnectionManager",idTransazione);
		}catch(Exception t) {
			throw new ConnettoreException(t.getMessage(),t);
		}
		try {
			if(!ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.containsKey(key)){
			
				try {			
					PoolingHttpClientConnectionManagerBuilder poolingConnectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
					
					if(tlsSocketStrategy!=null) {
						poolingConnectionManagerBuilder.setTlsSocketStrategy(tlsSocketStrategy);
					}
					else {
						poolingConnectionManagerBuilder.useSystemProperties();
					}
									
					// LAX: Higher concurrency but with lax connection max limit guarantees.
					// STRICT: Strict connection max limit guarantees.
					poolingConnectionManagerBuilder.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX);
	
					// FIFO: Re-use all connections equally preventing them from becoming idle and expiring.
					// LIFO: Re-use as few connections as possible making it possible for connections to become idle and expire.
					poolingConnectionManagerBuilder.setConnPoolPolicy(PoolReusePolicy.FIFO);
					
		
					// PoolingNHttpClientConnectionManager maintains a maximum limit of connection on a per route basis and in total. 
					// Per default this implementation will create no more than than 2 concurrent connections per given route and no more 20 connections in total.
					// For many real-world applications these limits may prove too constraining, especially if they use HTTP as a transport protocol for their services. 
					// Connection limits, however, can be adjusted using ConnPoolControl methods.
					// Increase max total connection
					ConnettoreHttpPoolParams poolParams = connectionConfig.getHttpPoolParams();
					Integer defaultMaxPerRoute = poolParams.getDefaultMaxPerRoute();
					Integer maxTotal = poolParams.getMaxTotal();
					if(maxTotal!=null && maxTotal>0) {
						poolingConnectionManagerBuilder.setMaxConnTotal(poolParams.getMaxTotal());
					}
					// Increase default max connection per route
					if(defaultMaxPerRoute!=null && defaultMaxPerRoute>0) {
						poolingConnectionManagerBuilder.setMaxConnPerRoute(poolParams.getDefaultMaxPerRoute());
					}
					// Increase max connections for localhost:80
					/**HttpHost localhost = new HttpHost("locahost", 80);
					poolingConnectionManagerBuilder.setMaxPerRoute(new HttpRoute(localhost), 50);*/
					
					ConnectionConfig config = buildConnectionConfig(poolParams, connectionConfig.getConnectionTimeout());
					poolingConnectionManagerBuilder.setDefaultConnectionConfig(config);
					
					PoolingHttpClientConnectionManager poolingConnectionManager = poolingConnectionManagerBuilder.build();
									
					/** Gestito con 'setSSLSocketFactory' 
		               if(sslConnectionSocketFactory!=null) {
		                       Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
		                               .<ConnectionSocketFactory> create().register("https", sslConnectionSocketFactory)
		                               .build();
		                       cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
		               }
		               else {
		                       cm = new PoolingHttpClientConnectionManager();
		               }
					 */
					
					ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.put(key, poolingConnectionManager);
					
				}catch(Exception t) {
					throw new ConnettoreException(t.getMessage(),t);
				}
			}
		}finally {
			semaphorePoolingConnectionManager.release(lock, "initPoolingConnectionManager",idTransazione);
		}
		
	}
	private static ConnectionConfig buildConnectionConfig(ConnettoreHttpPoolParams poolParams, long connectionTimeout) {
		OpenSPCoop2Properties op2Properties = OpenSPCoop2Properties.getInstance();
		Integer closeIdleConnectionsAfterSeconds = op2Properties.getBIOConfigSyncClientCloseIdleConnectionsAfterSeconds();
		boolean idleConnectionEvictorEnabled = (closeIdleConnectionsAfterSeconds!=null && closeIdleConnectionsAfterSeconds>0);
		int sleepTimeSeconds = -1;
		if(idleConnectionEvictorEnabled) {
			sleepTimeSeconds = op2Properties.getBIOConfigSyncClientCloseIdleConnectionsCheckIntervalSeconds();
		}
		return ConnettoreHTTPCOREUtils.buildConnectionConfig(poolParams, connectionTimeout, idleConnectionEvictorEnabled, sleepTimeSeconds);
	}
	
	
	public static synchronized void stop() throws ConnettoreException {
			
		List<Throwable> listT = new ArrayList<>();
		
		// Shut down the evictor thread
		if(ConnettoreHTTPCOREConnectionManager.idleConnectionEvictor!=null) {
			try {			
				ConnettoreHTTPCOREConnectionManager.idleConnectionEvictor.setStop(true);
				idleConnectionEvictor.waitShutdown();
			}catch(Exception t) {
				listT.add(t);
			}	
		}
		
		// close client
		stopClients(listT);
		
		// Shut down connManager
		stopConnectionManager(listT);
					
		// gestione eccezione
		throwExceptions(listT);
		
	}
	public static synchronized void restartConnectionManager() throws ConnettoreException {
		
		List<Throwable> listT = new ArrayList<>();
		
		// close client
		stopClients(listT);
		
		// Shut down connManager
		stopConnectionManager(listT);
		
		// gestione eccezione
		throwExceptions(listT);
		
	}
	private static void throwExceptions(List<Throwable> listT) throws ConnettoreException {
		if(listT!=null && !listT.isEmpty()) {
			if(listT.size()==1) {
				throw new ConnettoreException(listT.get(0).getMessage(),listT.get(0));		
			}
			else {
				UtilsMultiException multiExc = new UtilsMultiException(listT.toArray(new Throwable[1]));
				throw new ConnettoreException(multiExc.getMessage(),multiExc);		
			}
		}
	}
	
	private static void stopClients(List<Throwable> listT) throws ConnettoreException {
		if(mapConnection!=null && !mapConnection.isEmpty()) {
			Iterator<String> it = mapConnection.keySet().iterator();
			while (it.hasNext()) {
				String key = it.next();
				ConnettoreHTTPCOREConnection connection = mapConnection.get(key);
				try {
					connection.close(); // GRACEFUL tenta di chiudere le connessioni in modo ordinato, permettendo alle richieste in corso di completarsi.
				}catch(Exception t) {
					listT.add(new ConnettoreException("Connection ["+key+"] close error: "+t.getMessage(),t));
				}
			}
			mapConnection.clear();
		}
	}
	private static void stopConnectionManager(List<Throwable> listT) {
		if(ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager!=null && !ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.isEmpty()) {
			for (String key : ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.keySet()) {
				if(key!=null) {
					PoolingHttpClientConnectionManager cm = ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.get(key);
					stopConnectionManager(cm, listT);
				}
			}
			ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.clear();
		}
	}
	private static void stopConnectionManager(PoolingHttpClientConnectionManager cm, List<Throwable> listT) {
		if(cm!=null) {
			try {			
				cm.close(CloseMode.GRACEFUL); // tenta di chiudere le connessioni in modo ordinato, permettendo alle richieste in corso di completarsi.
			}catch(Exception t) {
				listT.add(t);
			}
		}
	}
	
	private static void init(ConnettoreHTTPCOREConnectionConfig connectionConfig,org.openspcoop2.security.keystore.SSLSocketFactory sslSocketFactory,
			Loader loader, ConnettoreLogger logger, 
			ConnectionKeepAliveStrategy keepAliveStrategy,
			ConnettoreHttpRequestInterceptor httpRequestInterceptor) throws ConnettoreException {
		String key = connectionConfig.toKeyConnection();
		String idTransazione = logger!=null ? logger.getIdTransazione() : null;
		SemaphoreLock lock = null;
		try {
			lock = semaphoreConnection.acquire("initConnection",idTransazione);
		}catch(Exception t) {
			throw new ConnettoreException(t.getMessage(),t);
		}
		try {
			if(!mapConnection.containsKey(key)) {
				ConnettoreHTTPCOREConnection resource = buildClient(connectionConfig, sslSocketFactory,
						loader, logger, key,
						keepAliveStrategy,
						httpRequestInterceptor);
				mapConnection.put(key, resource);
			}
		}finally {
			semaphoreConnection.release(lock, "initConnection",idTransazione);
		}
	}
	private static ConnettoreHTTPCOREConnection update(ConnettoreHTTPCOREConnectionConfig connectionConfig,org.openspcoop2.security.keystore.SSLSocketFactory sslSocketFactory,
			Loader loader, ConnettoreLogger logger, 
			ConnectionKeepAliveStrategy keepAliveStrategy,
			ConnettoreHttpRequestInterceptor httpRequestInterceptor) throws ConnettoreException {
		String key = connectionConfig.toKeyConnection();
		String idTransazione = logger!=null ? logger.getIdTransazione() : null;
		SemaphoreLock lock = null;
		try {
			lock = semaphoreConnection.acquire("updateConnection",idTransazione);
		}catch(Exception t) {
			throw new ConnettoreException(t.getMessage(),t);
		}
		try {
			if(mapConnection.containsKey(key)) {
				ConnettoreHTTPCOREConnection con = mapConnection.remove(key);
				mapConnection.put("expired_"+key+"_"+UUID.randomUUID().toString(), con);
			}
			ConnettoreHTTPCOREConnection resource = buildClient(connectionConfig, sslSocketFactory,
					loader, logger, key,
					keepAliveStrategy,
					httpRequestInterceptor);
			mapConnection.put(key, resource);
			return resource;
		}finally {
			semaphoreConnection.release(lock, "updateConnection",idTransazione);
		}
	}

	private static ConnettoreHTTPCOREConnection buildClient(ConnettoreHTTPCOREConnectionConfig connectionConfig,org.openspcoop2.security.keystore.SSLSocketFactory sslSocketFactory,
			Loader loader, ConnettoreLogger logger, String key,
			ConnectionKeepAliveStrategy keepAliveStrategy,
			ConnettoreHttpRequestInterceptor httpRequestInterceptor) throws ConnettoreException {
		try {				
						
			RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
			
			// ** Timeout **
			ConnettoreHTTPCOREUtils.setTimeout(requestConfigBuilder, connectionConfig);
			
			// ** Redirect **
			ConnettoreHTTPCOREUtils.setRedirect(requestConfigBuilder, connectionConfig);
						
			RequestConfig requestConfig = requestConfigBuilder.build();
			
			// Pool
			String keyPool = connectionConfig.toKeyConnectionManager();
			
			if(!ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.containsKey(keyPool)){
				
				TlsSocketStrategy tlsStrategy = buildSSLConnectionSocketFactory(connectionConfig,
						sslSocketFactory,
						loader, logger);
				ConnettoreHTTPCOREConnectionManager.initialize(keyPool, tlsStrategy, connectionConfig, logger);
			}
			PoolingHttpClientConnectionManager cm = ConnettoreHTTPCOREConnectionManager.mapPoolingConnectionManager.get(keyPool);
			
			HttpClientBuilder httpClientBuilder = HttpClients.custom();
			httpClientBuilder.setConnectionManager( cm );
			httpClientBuilder.setConnectionManagerShared(true); // senno' la close di una connessione fa si che venga chiuso il reactor
			httpClientBuilder.setDefaultRequestConfig(requestConfig);
			httpClientBuilder.disableAuthCaching();
			
			ConnectionReuseStrategy defaultClientConnectionReuseStrategy = new DefaultConnectionReuseStrategy();
			httpClientBuilder.setConnectionReuseStrategy(defaultClientConnectionReuseStrategy);
			
			if(keepAliveStrategy!=null){
				httpClientBuilder.setKeepAliveStrategy(keepAliveStrategy);
			}
					
			/**System.out.println("PRESA LA CONNESSIONE AVAILABLE["+cm.getTotalStats().getAvailable()+"] LEASED["
					+cm.getTotalStats().getLeased()+"] MAX["+cm.getTotalStats().getMax()+"] PENDING["+cm.getTotalStats().getPending()+"]");
			System.out.println("-----GET CONNECTION [END] ----");*/
			
			if(httpRequestInterceptor!=null) {
				httpClientBuilder.addRequestInterceptorLast(httpRequestInterceptor);
			}
			
			if(connectionConfig.isFollowRedirect()) {
				httpClientBuilder.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE);
			}
			
			// ** Proxy **
			setProxy(httpClientBuilder, connectionConfig);
			
			CloseableHttpClient httpclient = httpClientBuilder.build();
			
			OpenSPCoop2Properties op2 = OpenSPCoop2Properties.getInstance();
			int expireUnusedAfterSeconds = op2.getBIOConfigSyncClientExpireUnusedAfterSeconds(); 
			int closeUnusedAfterSeconds = op2.getBIOConfigSyncClientCloseUnusedAfterSeconds(); 
			
			return new ConnettoreHTTPCOREConnection(key, httpclient, requestConfig,
					expireUnusedAfterSeconds, closeUnusedAfterSeconds);
			
		} catch ( Exception t ) {
			throw new ConnettoreException( t.getMessage(),t );
		}
	}
	
	private static void setProxy(HttpClientBuilder httpClientBuilder, AbstractConnettoreConnectionConfig connectionConfig) {
		if(connectionConfig.getProxyHost()!=null && connectionConfig.getProxyPort()!=null) {
			HttpHost proxy = new HttpHost(connectionConfig.getProxyHost(), connectionConfig.getProxyPort());
			httpClientBuilder.setProxy(proxy);
		}
		else {
			// Quando non c'è un proxy configurato esplicitamente, usa useSystemProperties() per leggere le proprietà JAVA_OPTS
			// (http.proxyHost, http.proxyPort, https.proxyHost, https.proxyPort, ecc.)
			httpClientBuilder.useSystemProperties();
		}
	}
	
	private static TlsSocketStrategy buildSSLConnectionSocketFactory(AbstractConnettoreConnectionConfig connectionConfig,
			org.openspcoop2.security.keystore.SSLSocketFactory sslSocketFactory,
			Loader loader, ConnettoreLogger logger) throws UtilsException {
		TlsSocketStrategy tlsSocketStrategy = null;
		if(connectionConfig.getSslContextProperties()!=null) {
			
			StringBuilder bfLog = new StringBuilder();
			HostnameVerifier hostnameVerifier = SSLUtilities.generateHostnameVerifier(connectionConfig.getSslContextProperties(), bfLog, 
					logger.getLogger(), loader);
			if(connectionConfig.isDebug()) {
				logger.debug(bfLog.toString());
			}
			
			if(hostnameVerifier==null) {
				hostnameVerifier = new DefaultHostnameVerifier();
			}
			
			SSLContext sslContext = sslSocketFactory.getSSLContext();

			
			tlsSocketStrategy = new DefaultClientTlsStrategy(sslContext, hostnameVerifier);
			
			if(connectionConfig.isDebug()) {
				String clientCertificateConfigurated = connectionConfig.getSslContextProperties().getKeyStoreLocation();
				tlsSocketStrategy = new WrappedLogTlsSocketStrategy(tlsSocketStrategy, 
						logger.getLogger(), logger.buildMsg(""),
						clientCertificateConfigurated);
			}
			
		}
		return tlsSocketStrategy;
	}
	
	public static ConnettoreHTTPCOREConnection getConnettoreHTTPCOREConnection(ConnettoreHTTPCOREConnectionConfig connectionConfig,org.openspcoop2.security.keystore.SSLSocketFactory sslSocketFactory,
			Loader loader, ConnettoreLogger logger,
			ConnectionKeepAliveStrategy keepAliveStrategy,
			ConnettoreHttpRequestInterceptor httpRequestInterceptor) throws ConnettoreException  {
		
		ConnettoreHTTPCOREConnection connection = null;
		if(USE_POOL_CONNECTION) {
			String key = connectionConfig.toKeyConnection();
			if(!mapConnection.containsKey(key)) {
				init(connectionConfig, sslSocketFactory,
						loader, logger,
						keepAliveStrategy, httpRequestInterceptor);
				connection = mapConnection.get(key);
				connection.refresh();
			}
			else {
				connection = mapConnection.get(key);
				connection.refresh();
				if(connection.isExpired()) {
					connection = update(connectionConfig, sslSocketFactory,
							loader, logger,
							keepAliveStrategy, httpRequestInterceptor);
				}
			}
		}
		else {
			connection = buildClient(connectionConfig, sslSocketFactory,
					loader, logger, connectionConfig.toKeyConnection(),
					keepAliveStrategy,
					httpRequestInterceptor);
			connection.refresh();
		}
		return connection;
	}
	
}