HttpClientAddressSanitizer.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;

/**
 * HttpClientAddressSanitizer
 *
 * Sanitizza gli indirizzi IP di trasporto rimuovendo le informazioni sulla porta.
 *
 * Gestisce i formati prodotti dai seguenti header HTTP:
 *
 * 1) X-Forwarded-For / Forwarded-For / X-Forwarded / X-Client-IP / Client-IP / X-Cluster-Client-IP / Cluster-Client-IP
 *    Formato: lista di IP separati da virgola
 *    - IPv4:                192.168.1.1
 *    - IPv4 con porta:     192.168.1.1:8080
 *    - IPv6:               2001:db8::1
 *    - IPv6 con porta:     [2001:db8::1]:8080
 *    - IPv6 con brackets:  [2001:db8::1]
 *    - Multipli:           192.168.1.1:8080, 10.0.0.1:9090
 *
 * 2) Forwarded (RFC 7239)
 *    Formato: parametri chiave=valore separati da ';', entries separate da ','
 *    - for=192.0.2.43
 *    - for=192.0.2.43:47011
 *    - for="[2001:db8:cafe::17]:4711"
 *    - for="[2001:db8:cafe::17]"
 *    - for=192.0.2.60;proto=http;by=203.0.113.43
 *    - for=192.0.2.43, for=198.51.100.17
 *    Il parametro 'by' indica l'indirizzo del proxy (non del client) e viene scartato.
 *
 * @author Poli Andrea (poli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class HttpClientAddressSanitizer {

	private HttpClientAddressSanitizer() {}

	/**
	 * Sanitizza l'indirizzo di trasporto rimuovendo le informazioni sulla porta.
	 * Gestisce indirizzi multipli separati da virgola e il formato Forwarded (RFC 7239).
	 */
	public static String sanitizeTransportAddressPort(String transportAddress) {
		if(transportAddress==null || "".equals(transportAddress)) {
			return transportAddress;
		}
		// Gestisce indirizzi multipli separati da virgola
		// (es. "X-Forwarded-For: ip1, ip2" oppure "Forwarded: for=ip1, for=ip2")
		String[] addresses = transportAddress.split(",");
		StringBuilder result = new StringBuilder();
		for (int i = 0; i < addresses.length; i++) {
			String addr = addresses[i].trim();
			if("".equals(addr)) {
				continue;
			}
			if(result.length() > 0) {
				result.append(", ");
			}
			// Gestione formato Forwarded (RFC 7239): estrae l'IP dal parametro "for="
			// Il check evita di applicare la logica RFC 7239 a valori provenienti da altri header (X-Forwarded-For, X-Client-IP, ecc.)
			if(isForwardedHeaderFormat(addr)) {
				addr = parseForwardedHeaderValue(addr);
			}
			result.append(stripPort(addr));
		}
		return result.toString();
	}

	/*
	 * Gestisce il formato dell'header Forwarded (RFC 7239).
	 *
	 * Il valore puo' contenere parametri separati da ';' (es. "for=192.0.2.60;proto=http;by=203.0.113.43").
	 * Viene estratto il valore del parametro 'for' che contiene l'indirizzo del client.
	 * Il parametro 'by' (indirizzo del proxy) e 'proto' (protocollo) vengono scartati.
	 *
	 * Se il valore e' tra doppi apici (RFC 7239 richiede quoting per IPv6 e porte),
	 * i doppi apici vengono rimossi (es. for="[2001:db8::17]:4711" -> [2001:db8::17]:4711).
	 *
	 * Se il valore non e' in formato Forwarded (nessun parametro 'for='), viene restituito invariato.
	 */
	static String parseForwardedHeaderValue(String value) {
		String forValue = extractForParameter(value);
		if(forValue==null) {
			return value;
		}
		// Rimuove i doppi apici (RFC 7239: for="[2001:db8::17]:4711")
		if(forValue.length()>1 && forValue.startsWith("\"") && forValue.endsWith("\"")) {
			forValue = forValue.substring(1, forValue.length() - 1);
		}
		return forValue;
	}

	private static final String FORWARDED_FOR_PREFIX = "for=";
	private static final int FORWARDED_FOR_PREFIX_LENGTH = FORWARDED_FOR_PREFIX.length();

	/*
	 * Verifica se il valore ha il formato dell'header Forwarded (RFC 7239),
	 * cioe' inizia con 'for=' (case insensitive).
	 * Permette di distinguerlo dai valori provenienti da X-Forwarded-For, X-Client-IP, ecc.
	 * che contengono direttamente l'indirizzo IP senza prefissi.
	 */
	private static boolean isForwardedHeaderFormat(String value) {
		return value.length()>FORWARDED_FOR_PREFIX_LENGTH &&
				value.substring(0, FORWARDED_FOR_PREFIX_LENGTH).equalsIgnoreCase(FORWARDED_FOR_PREFIX);
	}

	/*
	 * Cerca il parametro 'for=' tra i parametri separati da ';' di un'entry dell'header Forwarded (RFC 7239).
	 * Restituisce il valore dopo 'for=' oppure null se non trovato.
	 */
	private static String extractForParameter(String value) {
		if(value.length()<=FORWARDED_FOR_PREFIX_LENGTH) {
			return null;
		}
		String[] params = value.split(";");
		for (int i = 0; i < params.length; i++) {
			String param = params[i].trim();
			if(param.length()>FORWARDED_FOR_PREFIX_LENGTH &&
					param.substring(0, FORWARDED_FOR_PREFIX_LENGTH).equalsIgnoreCase(FORWARDED_FOR_PREFIX)) {
				return param.substring(FORWARDED_FOR_PREFIX_LENGTH);
			}
		}
		return null;
	}

	/*
	 * Rimuove la porta da un singolo indirizzo IP.
	 *
	 * Logica di riconoscimento:
	 * - Inizia con '[':  IPv6 con brackets, eventualmente con porta -> [2001:db8::1]:8080 -> 2001:db8::1
	 * - Un solo ':':     IPv4 con porta (le cifre dopo ':' sono il numero di porta) -> 192.168.1.1:8080 -> 192.168.1.1
	 * - Piu' di un ':':  IPv6 senza porta (i ':' sono separatori di gruppi IPv6) -> 2001:db8::1 -> invariato
	 * - Zero ':':        indirizzo senza porta -> 192.168.1.1 -> invariato
	 */
	static String stripPort(String address) {
		if(address==null || "".equals(address)) {
			return address;
		}
		// IPv6 con brackets: [2001:db8::1]:8080 -> 2001:db8::1 ; [2001:db8::1] -> 2001:db8::1
		if(address.startsWith("[")) {
			int closeBracket = address.indexOf(']');
			if(closeBracket > 0) {
				return address.substring(1, closeBracket);
			}
			return address;
		}
		// IPv4 con porta: esattamente un ':' seguito da sole cifre
		if(isIpv4WithPort(address)) {
			return address.substring(0, address.indexOf(':'));
		}
		return address;
	}

	/*
	 * Verifica se l'indirizzo e' un IPv4 con porta (es. 192.168.1.1:8080).
	 * Un IPv4 con porta contiene esattamente un ':' e dopo il ':' ci sono solo cifre.
	 * Se ci sono piu' di un ':' si tratta di un indirizzo IPv6 (es. 2001:db8::1).
	 */
	private static boolean isIpv4WithPort(String address) {
		int colonCount = 0;
		int lastColon = -1;
		for (int i = 0; i < address.length(); i++) {
			if(address.charAt(i) == ':') {
				colonCount++;
				lastColon = i;
			}
		}
		if(colonCount != 1 || lastColon <= 0) {
			return false;
		}
		String afterColon = address.substring(lastColon + 1);
		if(afterColon.isEmpty()) {
			return false;
		}
		for (int i = 0; i < afterColon.length(); i++) {
			if(!Character.isDigit(afterColon.charAt(i))) {
				return false;
			}
		}
		return true;
	}
}