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"));
+  }
+}