You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/08/06 13:07:30 UTC
(hop) branch main updated: use proxy for all salesforce connection types, fixes #7795 (#7796)
This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 210e09b8f0 use proxy for all salesforce connection types, fixes #7795 (#7796)
210e09b8f0 is described below
commit 210e09b8f0e9d7a6373ec427d1cac35d256eb1ab
Author: Hans Van Akelyen <ha...@gmail.com>
AuthorDate: Thu Aug 6 15:07:24 2026 +0200
use proxy for all salesforce connection types, fixes #7795 (#7796)
---
.../salesforce/SalesforceConnection.java | 103 +++++++--
.../salesforce/SalesforceConnectionProxyTest.java | 237 +++++++++++++++++++++
2 files changed, 326 insertions(+), 14 deletions(-)
diff --git a/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnection.java b/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnection.java
index ab85411e5c..18ad4bf5a9 100644
--- a/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnection.java
+++ b/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnection.java
@@ -43,6 +43,9 @@ import com.sforce.ws.wsdl.Constants;
import jakarta.xml.soap.SOAPException;
import java.io.IOException;
import java.io.StringWriter;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -75,6 +78,7 @@ public class SalesforceConnection {
public static final String CONST_ARROW_LEFT = "<-----------------------------------------";
public static final String CONST_ARROW_RIGHT = "----------------------------------------->";
public static final String SERVICES_SOAP_U_64_0 = "/services/Soap/u/64.0";
+ private static final String HTTPS_SCHEME = "https://";
public static final String CONST_INVALID_SESSION_ID = "INVALID_SESSION_ID";
public static final String CONST_INVALID_SESSION_ID_ALTERNATE = "Invalid Session ID";
@@ -355,6 +359,84 @@ public class SalesforceConnection {
}
}
+ /**
+ * Apply the JVM proxy configuration to a connector config.
+ *
+ * <p>The Salesforce web service connector never consults the JVM proxy settings itself: it calls
+ * {@code URL.openConnection(config.getProxy())} and defaults that to {@link Proxy#NO_PROXY},
+ * which bypasses the default {@link ProxySelector} altogether. Every config we hand to a {@link
+ * PartnerConnection} therefore has to get its proxy set explicitly.
+ *
+ * @param config the connector config to configure
+ * @param endpoint the endpoint this config will connect to
+ */
+ private void applyProxy(ConnectorConfig config, String endpoint) {
+ Proxy proxy = selectProxy(endpoint);
+ if (proxy == null) {
+ return;
+ }
+ config.setProxy(proxy);
+
+ String proxyUser = System.getProperty("http.proxyUser", null);
+ if (StringUtils.isNotEmpty(proxyUser)) {
+ config.setProxyUsername(proxyUser);
+ config.setProxyPassword(
+ Encr.decryptPasswordOptionallyEncrypted(System.getProperty("http.proxyPassword", null)));
+ }
+
+ if (log.isDebug()) {
+ log.logDebug("Using proxy " + proxy + " to reach " + endpoint);
+ }
+ }
+
+ /**
+ * Determine the proxy to use for an endpoint.
+ *
+ * @param endpoint the endpoint that will be contacted
+ * @return the proxy to use, or null when the endpoint has to be reached directly
+ */
+ private Proxy selectProxy(String endpoint) {
+ Proxy proxy = selectProxyFor(endpoint);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ // No https proxy configured: keep applying http.proxyHost to the (https) Salesforce endpoints,
+ // which is what Hop has always done for username/password connections. Selecting again with
+ // the scheme swapped to http lets the JVM apply http.proxyHost and http.nonProxyHosts for us.
+ if (StringUtils.isEmpty(System.getProperty("https.proxyHost"))
+ && endpoint != null
+ && endpoint.startsWith(HTTPS_SCHEME)) {
+ return selectProxyFor("http://" + endpoint.substring(HTTPS_SCHEME.length()));
+ }
+ return null;
+ }
+
+ /** Ask the JVM which proxy applies to an endpoint, honouring the proxy system properties. */
+ private Proxy selectProxyFor(String endpoint) {
+ try {
+ for (Proxy proxy : ProxySelector.getDefault().select(new URI(endpoint))) {
+ if (proxy.type() != Proxy.Type.DIRECT) {
+ return proxy;
+ }
+ }
+ } catch (Exception e) {
+ // A bad endpoint is reported by the connection attempt itself, don't fail over it here.
+ if (log.isDebug()) {
+ log.logDebug("Unable to determine the proxy for " + endpoint + " : " + e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /** Open an HTTP connection to an endpoint, through the proxy configured for it if any. */
+ private java.net.HttpURLConnection openHttpConnection(String endpoint) throws IOException {
+ java.net.URL endpointUrl = new java.net.URL(endpoint);
+ Proxy proxy = selectProxy(endpoint);
+ return (java.net.HttpURLConnection)
+ (proxy == null ? endpointUrl.openConnection() : endpointUrl.openConnection(proxy));
+ }
+
private void connectWithUsernamePassword() throws HopException {
ConnectorConfig config = new ConnectorConfig();
config.setAuthEndpoint(getUrl());
@@ -364,16 +446,7 @@ public class SalesforceConnection {
config.setCompression(isUsingCompression());
config.setManualLogin(true);
- String proxyUrl = System.getProperty("http.proxyHost", null);
- if (StringUtils.isNotEmpty(proxyUrl)) {
- int proxyPort = Integer.parseInt(System.getProperty("http.proxyPort", "80"));
- String proxyUser = System.getProperty("http.proxyUser", null);
- String proxyPassword =
- Encr.decryptPasswordOptionallyEncrypted(System.getProperty("http.proxyPassword", null));
- config.setProxy(proxyUrl, proxyPort);
- config.setProxyUsername(proxyUser);
- config.setProxyPassword(proxyPassword);
- }
+ applyProxy(config, getUrl());
// Set timeout
if (getTimeOut() > 0) {
@@ -560,6 +633,8 @@ public class SalesforceConnection {
// Set OAuth session ID (access token) for authentication
config.setSessionId(getOauthAccessToken());
+ applyProxy(config, config.getServiceEndpoint());
+
this.binding = new PartnerConnection(config);
if (log.isDetailed()) {
@@ -613,6 +688,8 @@ public class SalesforceConnection {
config.setSessionId(accessToken);
config.setCompression(isUsingCompression());
+ applyProxy(config, config.getServiceEndpoint());
+
// Set timeout
if (getTimeOut() > 0) {
config.setConnectionTimeout(getTimeOut());
@@ -651,8 +728,7 @@ public class SalesforceConnection {
}
// Prepare HTTP request
- java.net.URL url = new java.net.URL(tokenUrl);
- java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
+ java.net.HttpURLConnection connection = openHttpConnection(tokenUrl);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setDoOutput(true);
@@ -864,8 +940,7 @@ public class SalesforceConnection {
tokenUrl += "services/oauth2/token";
// Prepare the request
- java.net.URL url = new java.net.URL(tokenUrl);
- java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
+ java.net.HttpURLConnection connection = openHttpConnection(tokenUrl);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setDoOutput(true);
diff --git a/plugins/tech/salesforce/src/test/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnectionProxyTest.java b/plugins/tech/salesforce/src/test/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnectionProxyTest.java
new file mode 100644
index 0000000000..955e92cc32
--- /dev/null
+++ b/plugins/tech/salesforce/src/test/java/org/apache/hop/pipeline/transforms/salesforce/SalesforceConnectionProxyTest.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.hop.pipeline.transforms.salesforce;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import com.sforce.ws.ConnectorConfig;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.security.KeyPair;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.TwoWayPasswordEncoderPluginType;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.util.EnvUtil;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mockito;
+
+/**
+ * Tests that every authentication path applies the JVM proxy configuration to the SOAP connector
+ * config. The Salesforce connector defaults to {@link Proxy#NO_PROXY}, which bypasses the JVM proxy
+ * selector, so a config without an explicit proxy connects directly and times out behind a proxy.
+ */
+@WireMockTest
+class SalesforceConnectionProxyTest {
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension();
+
+ private static final String PROXY_HOST = "proxy.example.com";
+ private static final String PROXY_PORT = "3128";
+ private static final String INSTANCE_URL = "https://na123.salesforce.com";
+
+ private static final String[] PROXY_PROPERTIES = {
+ "http.proxyHost",
+ "http.proxyPort",
+ "https.proxyHost",
+ "https.proxyPort",
+ "http.nonProxyHosts",
+ "http.proxyUser",
+ "http.proxyPassword"
+ };
+
+ private ILogChannel mockLog;
+ private String testPrivateKey;
+
+ @BeforeAll
+ static void setUpClass() throws HopException {
+ PluginRegistry.addPluginType(TwoWayPasswordEncoderPluginType.getInstance());
+ PluginRegistry.init();
+ String passwordEncoderPluginID =
+ Const.NVL(EnvUtil.getSystemProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN), "Hop");
+ Encr.init(passwordEncoderPluginID);
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ mockLog = Mockito.mock(ILogChannel.class);
+ KeyPair keyPair = SalesforceTestUtils.generateTestKeyPair();
+ testPrivateKey = SalesforceTestUtils.privateKeyToPem(keyPair.getPrivate());
+ clearProxyProperties();
+ }
+
+ @AfterEach
+ void tearDown() {
+ clearProxyProperties();
+ mockLog = null;
+ }
+
+ private void clearProxyProperties() {
+ for (String property : PROXY_PROPERTIES) {
+ System.clearProperty(property);
+ }
+ }
+
+ private SalesforceConnection jwtConnection(String tokenEndpoint) throws HopException {
+ return SalesforceConnection.createJwtConnection(
+ mockLog, "test.user@example.com", "3MVG9TestConsumerKey", testPrivateKey, tokenEndpoint);
+ }
+
+ private Proxy selectProxy(SalesforceConnection connection, String endpoint) throws Exception {
+ Method method = SalesforceConnection.class.getDeclaredMethod("selectProxy", String.class);
+ method.setAccessible(true);
+ return (Proxy) method.invoke(connection, endpoint);
+ }
+
+ private void stubTokenEndpoint(WireMockRuntimeInfo wmRuntimeInfo) {
+ wmRuntimeInfo
+ .getWireMock()
+ .register(
+ post(urlEqualTo("/services/oauth2/token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withBody(
+ SalesforceTestUtils.buildOAuthTokenResponse(
+ "00D5g000001JvToken!Test", null, INSTANCE_URL))));
+ }
+
+ /** Regression test for the OAuth JWT path connecting directly and timing out behind a proxy. */
+ @Test
+ void testConnectWithOAuthJwt_appliesProxyToSoapConfig(WireMockRuntimeInfo wmRuntimeInfo)
+ throws Exception {
+ stubTokenEndpoint(wmRuntimeInfo);
+ System.setProperty("https.proxyHost", PROXY_HOST);
+ System.setProperty("https.proxyPort", PROXY_PORT);
+
+ SalesforceConnection connection = jwtConnection(wmRuntimeInfo.getHttpBaseUrl());
+ connection.connect();
+
+ ConnectorConfig config = connection.getBinding().getConfig();
+ assertEquals(
+ new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(PROXY_HOST, 3128)),
+ config.getProxy(),
+ "The SOAP connector config must carry the configured proxy");
+ assertEquals(INSTANCE_URL + "/services/Soap/u/64.0", config.getServiceEndpoint());
+ }
+
+ @Test
+ void testConnectWithOAuthJwt_appliesProxyCredentials(WireMockRuntimeInfo wmRuntimeInfo)
+ throws Exception {
+ stubTokenEndpoint(wmRuntimeInfo);
+ System.setProperty("https.proxyHost", PROXY_HOST);
+ System.setProperty("https.proxyPort", PROXY_PORT);
+ System.setProperty("http.proxyUser", "proxyuser");
+ System.setProperty("http.proxyPassword", "proxypassword");
+
+ SalesforceConnection connection = jwtConnection(wmRuntimeInfo.getHttpBaseUrl());
+ connection.connect();
+
+ ConnectorConfig config = connection.getBinding().getConfig();
+ assertEquals("proxyuser", config.getProxyUsername());
+ assertEquals("proxypassword", config.getProxyPassword());
+ }
+
+ @Test
+ void testConnectWithOAuthJwt_noProxyConfigured(WireMockRuntimeInfo wmRuntimeInfo)
+ throws Exception {
+ stubTokenEndpoint(wmRuntimeInfo);
+
+ SalesforceConnection connection = jwtConnection(wmRuntimeInfo.getHttpBaseUrl());
+ connection.connect();
+
+ assertSame(
+ Proxy.NO_PROXY,
+ connection.getBinding().getConfig().getProxy(),
+ "Without proxy properties the connection must stay direct");
+ }
+
+ /** The Salesforce endpoints are https, so https.proxyHost has to be picked up. */
+ @Test
+ void testSelectProxy_httpsProxyHost() throws Exception {
+ System.setProperty("https.proxyHost", PROXY_HOST);
+ System.setProperty("https.proxyPort", PROXY_PORT);
+
+ Proxy proxy = selectProxy(jwtConnection("https://login.salesforce.com"), INSTANCE_URL);
+
+ assertNotNull(proxy);
+ assertEquals(PROXY_HOST, ((InetSocketAddress) proxy.address()).getHostString());
+ assertEquals(3128, ((InetSocketAddress) proxy.address()).getPort());
+ }
+
+ /** Hop has always applied http.proxyHost to the https Salesforce endpoints; keep doing that. */
+ @Test
+ void testSelectProxy_fallsBackToHttpProxyHost() throws Exception {
+ System.setProperty("http.proxyHost", PROXY_HOST);
+ System.setProperty("http.proxyPort", PROXY_PORT);
+
+ Proxy proxy = selectProxy(jwtConnection("https://login.salesforce.com"), INSTANCE_URL);
+
+ assertNotNull(proxy);
+ assertEquals(PROXY_HOST, ((InetSocketAddress) proxy.address()).getHostString());
+ assertEquals(3128, ((InetSocketAddress) proxy.address()).getPort());
+ }
+
+ @Test
+ void testSelectProxy_honoursNonProxyHosts() throws Exception {
+ System.setProperty("https.proxyHost", PROXY_HOST);
+ System.setProperty("https.proxyPort", PROXY_PORT);
+ System.setProperty("http.nonProxyHosts", "*.salesforce.com");
+
+ assertNull(selectProxy(jwtConnection("https://login.salesforce.com"), INSTANCE_URL));
+ }
+
+ @Test
+ void testSelectProxy_honoursNonProxyHostsOnTheHttpFallback() throws Exception {
+ System.setProperty("http.proxyHost", PROXY_HOST);
+ System.setProperty("http.proxyPort", PROXY_PORT);
+ System.setProperty("http.nonProxyHosts", "*.salesforce.com");
+
+ assertNull(selectProxy(jwtConnection("https://login.salesforce.com"), INSTANCE_URL));
+ }
+
+ @Test
+ void testSelectProxy_noProxyConfigured() throws Exception {
+ assertNull(selectProxy(jwtConnection("https://login.salesforce.com"), INSTANCE_URL));
+ }
+
+ @Test
+ void testSelectProxy_malformedEndpoint() throws Exception {
+ System.setProperty("https.proxyHost", PROXY_HOST);
+ System.setProperty("https.proxyPort", PROXY_PORT);
+
+ assertNull(selectProxy(jwtConnection("https://login.salesforce.com"), "not a valid uri"));
+ }
+}