diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index 8c6b50a275..37e032c05f 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -106,6 +106,8 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.net.MalformedURLException; +import java.net.URL; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -1266,7 +1268,7 @@ public static BObject createResponseStruct(HttpCarbonMessage httpCarbonMessage) } public static void populateSenderConfigurations(SenderConfiguration senderConfiguration, - BMap clientEndpointConfig, String scheme) { + BMap clientEndpointConfig, String scheme, String targetHost) { ProxyServerConfiguration proxyServerConfiguration; BMap secureSocket = (BMap) clientEndpointConfig .getMapValue(HttpConstants.ENDPOINT_CONFIG_SECURESOCKET); @@ -1308,6 +1310,11 @@ public static void populateSenderConfigurations(SenderConfiguration senderConfig proxyServerConfiguration.setProxyPassword(proxyPassword); } senderConfiguration.setProxyServerConfiguration(proxyServerConfiguration); + } else { + proxyServerConfiguration = resolveProxyFromEnv(scheme, targetHost); + if (proxyServerConfiguration != null) { + senderConfiguration.setProxyServerConfiguration(proxyServerConfiguration); + } } double timeout = ((BDecimal) clientEndpointConfig.get(HttpConstants.CLIENT_EP_ENDPOINT_TIMEOUT)).floatValue(); if (timeout < 0) { @@ -1322,6 +1329,96 @@ public static void populateSenderConfigurations(SenderConfiguration senderConfig senderConfiguration.setForwardedExtensionConfig(HttpUtil.getForwardedExtensionConfig(forwardedExtension)); } + static ProxyServerConfiguration resolveProxyFromEnv(String scheme, String targetHost) { + if (targetHost == null || targetHost.isEmpty()) { + return null; + } + String proxyEnv = null; + if (HttpConstants.PROTOCOL_HTTPS.equalsIgnoreCase(scheme)) { + proxyEnv = System.getenv("HTTPS_PROXY"); + if (proxyEnv == null) { + proxyEnv = System.getenv("https_proxy"); + } + } else if (HttpConstants.PROTOCOL_HTTP.equalsIgnoreCase(scheme)) { + proxyEnv = System.getenv("HTTP_PROXY"); + if (proxyEnv == null) { + proxyEnv = System.getenv("http_proxy"); + } + } + + if (proxyEnv == null || proxyEnv.trim().isEmpty()) { + return null; + } + + String noProxyEnv = System.getenv("NO_PROXY"); + if (noProxyEnv == null) { + noProxyEnv = System.getenv("no_proxy"); + } + + if (noProxyEnv != null && !noProxyEnv.trim().isEmpty()) { + if (shouldBypassProxy(targetHost, noProxyEnv)) { + return null; + } + } + + return parseProxyUrl(proxyEnv); + } + + static boolean shouldBypassProxy(String targetHost, String noProxyEnv) { + String host = targetHost.trim().toLowerCase(Locale.getDefault()); + String[] bypassList = noProxyEnv.split(","); + for (String bypass : bypassList) { + bypass = bypass.trim().toLowerCase(Locale.getDefault()); + if (bypass.isEmpty()) { + continue; + } + if ("*".equals(bypass)) { + return true; + } + if (bypass.startsWith(".")) { + if (host.endsWith(bypass) || host.equals(bypass.substring(1))) { + return true; + } + } else { + if (host.equals(bypass) || host.endsWith("." + bypass)) { + return true; + } + } + } + return false; + } + + static ProxyServerConfiguration parseProxyUrl(String proxyUrl) { + try { + String urlString = proxyUrl.trim(); + if (!urlString.contains("://")) { + urlString = "http://" + urlString; + } + URL url = new URL(urlString); + String host = url.getHost(); + int port = url.getPort(); + if (port == -1) { + port = HttpConstants.PROTOCOL_HTTPS.equalsIgnoreCase(url.getProtocol()) ? 443 : 80; + } + + ProxyServerConfiguration config = new ProxyServerConfiguration(host, port); + + String userInfo = url.getUserInfo(); + if (userInfo != null && !userInfo.isEmpty()) { + String[] parts = userInfo.split(":", 2); + String username = parts[0]; + config.setProxyUsername(username); + if (parts.length > 1) { + String password = parts[1]; + config.setProxyPassword(password); + } + } + return config; + } catch (MalformedURLException | UnknownHostException e) { + throw new BallerinaConnectorException("Invalid proxy URL: " + proxyUrl, e); + } + } + public static ConnectionManager getConnectionManager(BMap poolStruct) { ConnectionManager poolManager = (ConnectionManager) poolStruct.getNativeData(HttpConstants.CONNECTION_MANAGER); if (poolManager == null) { diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java index aa25296964..94ee3e9ee8 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java @@ -130,7 +130,7 @@ public static Object createSimpleHttpClient(BObject httpClient, BMap globalPoolC responseLimits.getIntValue(HttpConstants.MAX_ENTITY_BODY_SIZE), senderConfiguration.getMsgSizeValidationConfig()); try { - populateSenderConfigurations(senderConfiguration, clientEndpointConfig, scheme); + populateSenderConfigurations(senderConfiguration, clientEndpointConfig, scheme, url.getHost()); } catch (RuntimeException e) { return HttpUtil.createHttpError(e.getMessage(), HttpErrorType.GENERIC_CLIENT_ERROR); } diff --git a/native/src/test/java/io/ballerina/stdlib/http/api/ProxyEnvVarTest.java b/native/src/test/java/io/ballerina/stdlib/http/api/ProxyEnvVarTest.java new file mode 100644 index 0000000000..c1e2ae8dbb --- /dev/null +++ b/native/src/test/java/io/ballerina/stdlib/http/api/ProxyEnvVarTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.api; + +import io.ballerina.stdlib.http.transport.contract.config.ProxyServerConfiguration; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ProxyEnvVarTest { + + @Test + public void testShouldBypassProxy() { + // Exact match + Assert.assertTrue(HttpUtil.shouldBypassProxy("localhost", "localhost")); + Assert.assertTrue(HttpUtil.shouldBypassProxy("example.com", "example.com")); + + // Exact match case-insensitivity + Assert.assertTrue(HttpUtil.shouldBypassProxy("Example.Com", "example.com")); + Assert.assertTrue(HttpUtil.shouldBypassProxy("example.com", "EXAMPLE.COM")); + + // Wildcard + Assert.assertTrue(HttpUtil.shouldBypassProxy("example.com", "*")); + + // Suffix match + Assert.assertTrue(HttpUtil.shouldBypassProxy("sub.example.com", "example.com")); + Assert.assertTrue(HttpUtil.shouldBypassProxy("sub.example.com", ".example.com")); + Assert.assertTrue(HttpUtil.shouldBypassProxy("example.com", ".example.com")); + + // No match + Assert.assertFalse(HttpUtil.shouldBypassProxy("another-example.com", "example.com")); + Assert.assertFalse(HttpUtil.shouldBypassProxy("example.org", "example.com")); + + // List match + Assert.assertTrue(HttpUtil.shouldBypassProxy("localhost", "example.com, localhost, google.com")); + Assert.assertTrue(HttpUtil.shouldBypassProxy("sub.example.com", "example.com, localhost")); + } + + @Test + public void testParseProxyUrl() { + // Without scheme + ProxyServerConfiguration config1 = HttpUtil.parseProxyUrl("proxy.example.com:8080"); + Assert.assertEquals(config1.getProxyHost(), "proxy.example.com"); + Assert.assertEquals(config1.getProxyPort(), 8080); + Assert.assertNull(config1.getProxyUsername()); + Assert.assertNull(config1.getProxyPassword()); + + // With scheme (http) + ProxyServerConfiguration config2 = HttpUtil.parseProxyUrl("http://proxy.example.com:8080"); + Assert.assertEquals(config2.getProxyHost(), "proxy.example.com"); + Assert.assertEquals(config2.getProxyPort(), 8080); + + // With credentials + ProxyServerConfiguration config3 = HttpUtil.parseProxyUrl("http://user:pass@proxy.example.com:8080"); + Assert.assertEquals(config3.getProxyHost(), "proxy.example.com"); + Assert.assertEquals(config3.getProxyPort(), 8080); + Assert.assertEquals(config3.getProxyUsername(), "user"); + Assert.assertEquals(config3.getProxyPassword(), "pass"); + + // Without port + ProxyServerConfiguration config4 = HttpUtil.parseProxyUrl("http://proxy.example.com"); + Assert.assertEquals(config4.getProxyHost(), "proxy.example.com"); + Assert.assertEquals(config4.getProxyPort(), 80); + + // HTTPS scheme without port + ProxyServerConfiguration config5 = HttpUtil.parseProxyUrl("https://proxy.example.com"); + Assert.assertEquals(config5.getProxyHost(), "proxy.example.com"); + Assert.assertEquals(config5.getProxyPort(), 443); + } +}