HttpCoreConnection.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2026 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.utils.transport.http;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
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.protocol.HttpClientContext;
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.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.openspcoop2.utils.LoggerWrapperFactory;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.io.Base64Utilities;


/**
 * HttpCoreConnection
 *
 * @author Tommaso Burlon (tommaso.burlon@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
class HttpCoreConnection extends HttpLibraryConnection {

	
	private HttpHost setupProxy(HttpClientBuilder builder, Map<String, List<String>> addHeaders, HttpRequest request) {
		if (request.getProxyType() == null || request.getProxyHostname() == null) {
			// Quando non c'è un proxy configurato esplicitamente, usa useSystemProperties() per leggere le proprietà JAVA_OPTS
			// (http.proxyHost, http.proxyPort, https.proxyHost, https.proxyPort, ecc.)
			builder.useSystemProperties();
			if(request.isDebug()) {
				request.logInfo("Impostazione connessione alla URL ["+request.getUrl()+"] - Utilizzo proprietà di sistema (JAVA_OPTS) per proxy se configurate");
			}
			return null;
		}

		request.logInfo("Impostazione connessione alla URL ["+request.getUrl()+"] (via proxy "+
				request.getProxyHostname()+":"+request.getProxyPort()+") (username["+request.getProxyUsername()+"] password["+request.getProxyPassword()+"])...");

		HttpHost proxy = new HttpHost(request.getProxyHostname(), request.getProxyPort());
        builder.setProxy(proxy);

        // Proxy Authentication BASIC
		if(request.getProxyUsername() != null && request.getProxyPassword() != null){
			String authentication = request.getProxyUsername() + ":" + request.getProxyPassword();
			authentication = HttpConstants.AUTHORIZATION_PREFIX_BASIC + Base64Utilities.encodeAsString(authentication.getBytes());
			addHeaders.put(HttpConstants.PROXY_AUTHORIZATION, List.of(authentication));
		}

		return proxy;
	}
	
	private void enableRedirect(HttpClientBuilder builder, HttpRequest request) {
		if (Boolean.TRUE.equals(request.getFollowRedirects())) {
			if(request.isDebug()) {
        		request.logInfo("Redirect strategy abilitato");
        	}
        	DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
        	builder.setRedirectStrategy(redirectStrategy);
        } else {
        	if(request.isDebug()) {
        		request.logInfo("Redirect strategy disabilitato");
        	}
        	builder.disableRedirectHandling();
        }
	}
	
	private PoolingHttpClientConnectionManager setupConnectionManager(HttpClientBuilder builder, HttpRequest request, SSLContext sslContext) {

		PoolingHttpClientConnectionManager connManager = null;

		// Configurazione della connessione con timeout
		org.apache.hc.client5.http.config.ConnectionConfig.Builder connectionConfigBuilder =
				org.apache.hc.client5.http.config.ConnectionConfig.custom();

		// Imposta il connect timeout nella ConnectionConfig
		if(request.isDebug())
			request.logInfo("Impostazione connection config CT["+request.getConnectTimeout()+"]");
		connectionConfigBuilder.setConnectTimeout(request.getConnectTimeout(), java.util.concurrent.TimeUnit.MILLISECONDS);

		org.apache.hc.client5.http.config.ConnectionConfig connectionConfig = connectionConfigBuilder.build();

		// Set SSL context if provided
        if (sslContext != null) {
        	// 1. Create the socket factory
        	HostnameVerifier hostnameVerifier = request.isHostnameVerifier() ? new DefaultHostnameVerifier() : new SSLHostNameVerifierDisabled(LoggerWrapperFactory.getLogger(HttpCoreConnection.class));
        	if(request.isDebug() && request.isHostnameVerifier()) {
        		request.logInfo("HostName verifier abilitato");
        	}
        	else {
        		request.logInfo("HostName verifier disabilitato");
        	}

        	TlsSocketStrategy tlsSocketStrategy = new DefaultClientTlsStrategy(sslContext, hostnameVerifier);
        	if(request.isDebug()) {
				String clientCertificateConfigurated = request.getKeyStorePath();
				tlsSocketStrategy = new WrappedLogTlsSocketStrategy(tlsSocketStrategy,
						request.getLog(), "",
						clientCertificateConfigurated);
			}

        	// 2. Build the connection manager con ConnectionConfig
        	connManager = PoolingHttpClientConnectionManagerBuilder.create()
        	    .setTlsSocketStrategy(tlsSocketStrategy)
        	    .setDefaultConnectionConfig(connectionConfig)
        	    .build();

        	builder.setConnectionManager(connManager);

        }
        else {
        	// Quando sslContext non è fornito, usa useSystemProperties() per leggere le proprietà JAVA_OPTS
        	// (javax.net.ssl.trustStore, javax.net.ssl.keyStore, ecc.)
        	// Stesso comportamento di ConnettoreHTTPCOREConnectionManager.java:135
        	if(request.isDebug() && request.getUrl() != null && request.getUrl().toLowerCase().startsWith("https://")) {
        		String clientCertificateConfigurated = SSLUtilities.getJvmHttpsClientCertificateConfigurated();
        		request.logInfo("Utilizzo proprietà di sistema (JAVA_OPTS) per SSL - Client certificate: " + clientCertificateConfigurated);
        	}
        	connManager = PoolingHttpClientConnectionManagerBuilder.create()
        			.useSystemProperties()
        			.setDefaultConnectionConfig(connectionConfig)
        			.build();
        	builder.setConnectionManager(connManager);
        }

        connManager.setMaxTotal(1);
        connManager.setDefaultMaxPerRoute(1);

        return connManager;
	}
	
	private void addHeader(HttpUriRequestBase httpRequest, Map<String, List<String>> overrideHeaders, HttpRequest request) {
		for (Map.Entry<String, List<String>> entry : overrideHeaders.entrySet()) {
        	for (String value : entry.getValue()) {
        		if(request.isDebug()) {
	        		request.logInfo("Aggiungo header (override) ["+entry.getKey()+"]=["+value+"]");
	        	}
                httpRequest.addHeader(entry.getKey(), value);
            }
        }
        
        for (Map.Entry<String, List<String>> entry : request.getHeadersValues().entrySet()) {
            if (overrideHeaders.containsKey(entry.getKey()))
            	continue;
        	for (String value : entry.getValue()) {
        		if(request.isDebug()) {
	        		request.logInfo("Aggiungo header ["+entry.getKey()+"]=["+value+"]");
	        	}
                httpRequest.addHeader(entry.getKey(), value);
            }
        }
        
        addHeaderContentType(httpRequest, request);
        
        addHeaderAuthorizationBasic(httpRequest, request);
        addHeaderAuthorizationBearer(httpRequest, request);
        addHeaderAuthorizationApiKey(httpRequest, request);
	}
	private void addHeaderContentType(HttpUriRequestBase httpRequest, HttpRequest request) {
        // Content-Type
        if (request.getContentType() != null) {
        	if(request.isDebug()) {
        		request.logInfo("Impostazione Content-Type ["+request.getContentType()+"]");
        	}
            httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, request.getContentType());
        }
	}
	private void addHeaderAuthorizationBasic(HttpUriRequestBase httpRequest, HttpRequest request) {
        // Auth - Basic
        if (request.getUsername() != null && request.getPassword() != null) {
        	String authentication = request.getUsername() + ":" + request.getPassword();
			authentication = HttpConstants.AUTHORIZATION_PREFIX_BASIC + Base64Utilities.encodeAsString(authentication.getBytes());
			if(request.isDebug())
				request.logInfo("Impostazione autenticazione (username:"+request.getUsername()+" password:"+request.getPassword()+") ["+authentication+"]");
            httpRequest.setHeader(HttpHeaders.AUTHORIZATION, authentication);
        }
	}
	private void addHeaderAuthorizationBearer(HttpUriRequestBase httpRequest, HttpRequest request) {
        // Auth - Bearer
		if(request.getBearerToken()!=null){
			String authorizationHeader = HttpConstants.AUTHORIZATION_PREFIX_BEARER+request.getBearerToken();
			if(request.isDebug())
				request.logInfo("Impostazione autenticazione bearer ["+authorizationHeader+"]");
			httpRequest.setHeader(HttpConstants.AUTHORIZATION,authorizationHeader);
		}
	}
	private void addHeaderAuthorizationApiKey(HttpUriRequestBase httpRequest, HttpRequest request) {
		// Authentication Api Key
		String apiKey = request.getApiKey();
		if(apiKey!=null && StringUtils.isNotEmpty(apiKey)){
			String apiKeyHeader = request.getApiKeyHeader();
			if(apiKeyHeader==null || StringUtils.isEmpty(apiKeyHeader)) {
				apiKeyHeader = HttpConstants.AUTHORIZATION_HEADER_API_KEY;
			}
			httpRequest.setHeader(apiKeyHeader,apiKey);
			if(request.isDebug())
				request.logInfo("Impostazione autenticazione api key ["+apiKeyHeader+"]=["+apiKey+"]");
			
			addHeaderAuthorizationAppId(httpRequest, request);
		}
	}
	private void addHeaderAuthorizationAppId(HttpUriRequestBase httpRequest, HttpRequest request) {
		String appId = request.getAppId();
		if(appId!=null && StringUtils.isNotEmpty(appId)){
			String appIdHeader = request.getAppIdHeader();
			if(appIdHeader==null || StringUtils.isEmpty(appIdHeader)) {
				appIdHeader = HttpConstants.AUTHORIZATION_HEADER_APP_ID;
			}
			httpRequest.setHeader(appIdHeader,appId);
			if(request.isDebug())
				request.logInfo("Impostazione autenticazione api key (app id) ["+appIdHeader+"]=["+appId+"]");
		}
	}
	
	private void setupTimeouts(RequestConfig.Builder builder, HttpRequest request) {
		// Il connectTimeout è ora gestito tramite ConnectionConfig nel setupConnectionManager
		// Qui impostiamo solo connectionRequestTimeout e responseTimeout
		if(request.isDebug())
			request.logInfo("Impostazione request config timeout: connectionRequest["+request.getConnectTimeout()+"] response["+request.getReadTimeout()+"]");
		builder.setConnectionRequestTimeout(Timeout.ofMilliseconds(request.getConnectTimeout()))
        	.setResponseTimeout(Timeout.ofMilliseconds(request.getReadTimeout()));
	}
	
	private void setupMaxRedirect(RequestConfig.Builder builder, HttpRequest request) {
		if(request.isDebug())
			request.logInfo("Impostazione redirect max hop ["+request.getMaxHopRedirects()+"]");
		builder.setMaxRedirects(request.getMaxHopRedirects());
	}
	
	@Override
	public HttpResponse send(HttpRequest request, SSLContext sslContext, OCSPTrustManager ocspTrustManager) throws UtilsException, IOException {
	    
		
		if(request.getMethod()==null){
			throw new UtilsException("HttpMethod required");
		}
		
		PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = null;
		try {
	        // builder per la creazione del client
	        HttpClientBuilder builder = HttpClients.custom();
	        builder.setConnectionManagerShared(false);
	        
	        Map<String, List<String>> overrideHeaders = new HashMap<>();
	        
	        // abilito o disabilito i redirect
	        enableRedirect(builder, request);
	        
	        // aggiungo il proxy se necessario
	        HttpHost proxy = setupProxy(builder, overrideHeaders, request);

	        // imposto la gestione SSL
	        poolingHttpClientConnectionManager = setupConnectionManager(builder, request, sslContext);

	        // creo la richiesta
	        HttpUriRequestBase httpRequest = new HttpUriRequestBase(
	                request.getMethod().name(), URI.create(request.getUrl()));

	        // aggiungo gli headers
	        addHeader(httpRequest, overrideHeaders, request);

	        // imposto i timeout e il massimo di redirect
	        RequestConfig.Builder configBuilder = RequestConfig.custom();
	        setupMaxRedirect(configBuilder, request);
	        setupTimeouts(configBuilder, request);
	        
	        // set impostazioni
	        builder.evictExpiredConnections();
	        builder.evictIdleConnections(TimeValue.ofSeconds(1));
	        builder.disableAutomaticRetries();
	        httpRequest.setConfig(configBuilder.build());

	        // gestione del body
	        InputStream contentStream = null;
	        if (request.getContent() != null && request.getContent().length > 0) {
	        	contentStream = new ByteArrayInputStream(request.getContent());
	        } else if (request.getContentStream() != null) {
	        	contentStream = request.getContentStream();
	        }
	        
	        if (contentStream != null) {
	            // chunked or throttled stream
	            Integer contentLength = request.getContent().length;
	            
	            if (request.getThrottlingSendByte() != null && request.getThrottlingSendMs() != null) {
	            	contentStream = new ChunkedInputStream(contentStream, request.getThrottlingSendByte(), request.getThrottlingSendMs());
	                contentLength = -1;
	            }
	            
	            if (request.isForceTransferEncodingChunked())
	            	contentLength = -1;
	         
	            InputStreamEntity entity = new InputStreamEntity(
	            		contentStream, contentLength, ContentType.parse(request.getContentType()));
	            httpRequest.setEntity(entity);
	        }

	        CloseableHttpClient client = null;
	        ClassicHttpResponse httpResp = null;
	        try {
	        	client = builder.build();
		        if(request.isDebug())
		        	request.logDebug("Connessione in corso ...");
		        httpResp = client.executeOpen(proxy, httpRequest, HttpClientContext.create());
		        
		        // === Build response ===
		        HttpResponse response = new HttpResponse();
		        response.setResultHTTPOperation(httpResp.getCode());
		        
		        // Verifica solo della connessione
	 			if(request.isCheckConnection()) {
	 				return checkConnection(request) ;
	 			}
	 			else {		
	 				return processResponse(httpResp, response, ocspTrustManager);
	 			}
	        }finally {
	        	safeClose(httpResp);
	        	safeDisconnect(client);
	        }
	        
	    } catch (IOException e) {
	    	throw e;
	    } catch (Exception e) {
	        throw new UtilsException(e);
	    }finally {
	    	safeDisconnect(poolingHttpClientConnectionManager);
	    }
	}
	
	private HttpResponse checkConnection(HttpRequest request) {
		if(request.isDebug())
			request.logDebug("Connessione effettuata con successo");
		return null; // uguale a classe UrlConnectionConnection
	}
	private void safeClose(ClassicHttpResponse httpResp) {
		try {
			if(httpResp!=null) {
				httpResp.close();
			}
		}catch(Exception ignore) {
			// ignore
		}
	}
	private void safeDisconnect(CloseableHttpClient client) {
		try {
			if(client!=null) {
				client.close();
			}
		}catch(Exception ignore) {
			// ignore
		}
	}
	private void safeDisconnect(PoolingHttpClientConnectionManager cManager) {
		try {
			if(cManager!=null) {
				cManager.close(CloseMode.IMMEDIATE);
			}
		}catch(Exception ignore) {
			// ignore
		}
	}
	
	private HttpResponse processResponse(ClassicHttpResponse httpResp, HttpResponse response, OCSPTrustManager ocspTrustManager) throws IOException {
		Header[] responseHeaders = httpResp.getHeaders();
        for (Header h : responseHeaders) {
        	if(HttpConstants.CONTENT_TYPE.equalsIgnoreCase(h.getName()) &&
        			response.getContentType()==null // imposto il primo che incontro
        			) {
        		response.setContentType(h.getValue());
        	}
            response.addHeader(h.getName(), List.of(h.getValue()));
        }

        HttpEntity entity = httpResp.getEntity();
        if (entity != null) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            entity.writeTo(out);
            out.flush();
            out.close();
            response.setContent(out.toByteArray());
            // Consuma completamente l'entity e chiude lo stream sottostante
            EntityUtils.consumeQuietly(entity);
        }
        
        // per mantenere la retro compatibilità con urlConnection
        String returnCode = httpResp.getVersion() + " " + httpResp.getCode();
        if(httpResp.getReasonPhrase()!=null && StringUtils.isNotEmpty(httpResp.getReasonPhrase())) {
        	returnCode+= " " + httpResp.getReasonPhrase();
        }
        response.addHeader("ReturnCode", List.of(returnCode));
        
        // certificati server
        if(ocspTrustManager!=null) {
     		response.setServerCertificate(ocspTrustManager.getPeerCertificates());
     	}
     			
        return response;
	}

}