[🍒] [PLUGIN-1959] Add proxy support for ServiceNow plugins - #152
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces proxy support for ServiceNow connections, adding Proxy URL, Username, and Password configuration properties across batch sinks, sources, and connectors, along with a new ProxyOltuHttpClient to route OAuth token requests. The reviewer feedback identifies several improvement opportunities, including refining macro validation logic when no proxy is configured, ensuring a Proxy URL is provided if credentials are set, correctly handling HTTP PUT requests in ProxyOltuHttpClient, explicitly specifying UTF-8 encoding for entity parsing, and using a case-insensitive map for HTTP headers.
| !containsMacro(ServiceNowConstants.PROPERTY_PROXY_URL) && | ||
| !containsMacro(ServiceNowConstants.PROPERTY_PROXY_USERNAME) && | ||
| !containsMacro(ServiceNowConstants.PROPERTY_PROXY_PASSWORD); |
There was a problem hiding this comment.
If the proxy is not configured (i.e., proxyUrl is empty), we should not prevent connection validation just because proxyUsername or proxyPassword contain macros. We should only check if the proxy credentials contain macros if a proxy is actually configured.
!containsMacro(ServiceNowConstants.PROPERTY_PROXY_URL) &&
(connection == null || !connection.hasProxyConfigured() ||
(!containsMacro(ServiceNowConstants.PROPERTY_PROXY_USERNAME) &&
!containsMacro(ServiceNowConstants.PROPERTY_PROXY_PASSWORD)));| boolean hasUsername = !Util.isNullOrEmpty(proxyUsername); | ||
| boolean hasPassword = !Util.isNullOrEmpty(proxyPassword); | ||
| if (hasUsername && !hasPassword) { | ||
| collector.addFailure("Proxy password must be specified when a proxy username is provided.", null) | ||
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_PASSWORD); | ||
| } else if (!hasUsername && hasPassword) { | ||
| collector.addFailure("Proxy username must be specified when a proxy password is provided.", null) | ||
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_USERNAME); | ||
| } |
There was a problem hiding this comment.
If a user provides proxy credentials (username or password) but leaves the Proxy URL empty, the proxy settings will be silently ignored. We should validate that the Proxy URL is specified when proxy credentials are provided to prevent silent misconfigurations.
| boolean hasUsername = !Util.isNullOrEmpty(proxyUsername); | |
| boolean hasPassword = !Util.isNullOrEmpty(proxyPassword); | |
| if (hasUsername && !hasPassword) { | |
| collector.addFailure("Proxy password must be specified when a proxy username is provided.", null) | |
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_PASSWORD); | |
| } else if (!hasUsername && hasPassword) { | |
| collector.addFailure("Proxy username must be specified when a proxy password is provided.", null) | |
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_USERNAME); | |
| } | |
| boolean hasUsername = !Util.isNullOrEmpty(proxyUsername); | |
| boolean hasPassword = !Util.isNullOrEmpty(proxyPassword); | |
| if ((hasUsername || hasPassword) && Util.isNullOrEmpty(proxyUrl)) { | |
| collector.addFailure("Proxy URL must be specified when proxy credentials are provided.", null) | |
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_URL); | |
| } else if (hasUsername && !hasPassword) { | |
| collector.addFailure("Proxy password must be specified when a proxy username is provided.", null) | |
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_PASSWORD); | |
| } else if (!hasUsername && hasPassword) { | |
| collector.addFailure("Proxy username must be specified when a proxy password is provided.", null) | |
| .withConfigProperty(ServiceNowConstants.PROPERTY_PROXY_USERNAME); | |
| } |
| if (OAuth.HttpMethod.POST.equalsIgnoreCase(requestMethod) || OAuth.HttpMethod.PUT.equalsIgnoreCase(requestMethod)) { | ||
| HttpPost httpPost = new HttpPost(request.getLocationUri()); | ||
| if (request.getBody() != null) { | ||
| httpPost.setEntity(new StringEntity(request.getBody(), StandardCharsets.UTF_8)); | ||
| } | ||
| httpRequest = httpPost; | ||
| } else { | ||
| httpRequest = new HttpGet(request.getLocationUri()); | ||
| } |
There was a problem hiding this comment.
Using HttpPost for a PUT request is incorrect. If the request method is PUT, we should instantiate and use HttpPut instead of HttpPost.
if (OAuth.HttpMethod.POST.equalsIgnoreCase(requestMethod)) {
HttpPost httpPost = new HttpPost(request.getLocationUri());
if (request.getBody() != null) {
httpPost.setEntity(new StringEntity(request.getBody(), StandardCharsets.UTF_8));
}
httpRequest = httpPost;
} else if (OAuth.HttpMethod.PUT.equalsIgnoreCase(requestMethod)) {
org.apache.http.client.methods.HttpPut httpPut = new org.apache.http.client.methods.HttpPut(request.getLocationUri());
if (request.getBody() != null) {
httpPut.setEntity(new StringEntity(request.getBody(), StandardCharsets.UTF_8));
}
httpRequest = httpPut;
} else {
httpRequest = new HttpGet(request.getLocationUri());
}| int responseCode = response.getStatusLine().getStatusCode(); | ||
| String contentType = response.getEntity() != null && response.getEntity().getContentType() != null | ||
| ? response.getEntity().getContentType().getValue() : null; | ||
| String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : ""; |
There was a problem hiding this comment.
Calling EntityUtils.toString without specifying a charset defaults to the system default charset, which can lead to platform-dependent encoding issues (e.g., on Windows environments). It is safer to explicitly specify StandardCharsets.UTF_8.
| String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : ""; | |
| String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8) : ""; |
| Map<String, List<String>> responseHeaders = new java.util.HashMap<>(); | ||
| for (Header header : response.getAllHeaders()) { | ||
| List<String> values = responseHeaders.computeIfAbsent(header.getName(), k -> new ArrayList<>()); | ||
| values.add(header.getValue()); | ||
| } | ||
| return Collections.unmodifiableMap(responseHeaders); |
There was a problem hiding this comment.
HTTP header names are case-insensitive per RFC 7230. Using a standard HashMap can cause issues if headers are looked up with different casing (e.g., Content-Type vs content-type). It is safer to use a case-insensitive map, such as a TreeMap with String.CASE_INSENSITIVE_ORDER.
| Map<String, List<String>> responseHeaders = new java.util.HashMap<>(); | |
| for (Header header : response.getAllHeaders()) { | |
| List<String> values = responseHeaders.computeIfAbsent(header.getName(), k -> new ArrayList<>()); | |
| values.add(header.getValue()); | |
| } | |
| return Collections.unmodifiableMap(responseHeaders); | |
| Map<String, List<String>> responseHeaders = new java.util.TreeMap<>(String.CASE_INSENSITIVE_ORDER); | |
| for (Header header : response.getAllHeaders()) { | |
| List<String> values = responseHeaders.computeIfAbsent(header.getName(), k -> new ArrayList<>()); | |
| values.add(header.getValue()); | |
| } | |
| return Collections.unmodifiableMap(responseHeaders); |
Route all ServiceNow API calls (table reads, schema fetch, batch sink writes, OAuth token requests) through an optional HTTP proxy, modeled on the http-plugins proxy support. - Add proxyUrl, proxyUsername, proxyPassword properties to ServiceNowConnectorConfig (nullable, macro-enabled) with validation - Build a proxy-aware Apache HttpClient in RestAPIClient used by all GET/POST/DELETE calls - Route the OAuth token call through the proxy via a new ProxyOltuHttpClient (Oltu HttpClient backed by Apache HttpClient); falls back to URLConnectionClient when no proxy is configured - Add Proxy fields and a "Proxy authentication" filter to the connector, source, multi-source and sink widgets - Document the new properties and add unit tests for proxy config - ProxyOltuHttpClientTest: covers the OAuth-over-proxy client (POST token parsing, GET path, IOException wrapped as OAuthSystemException) - RestAPIClientProxyTest: covers getHttpClientBuilder() proxy wiring (proxy host + credentials, proxy-only, no-proxy, invalid URL) by inspecting the real HttpClientBuilder's fields, since its setters are final add commons-codec dependency for proxy authentication Added validation checks for Proxy Properties fixed errorMessage.properties updated docs for proxyURL
b0e5a33 to
a6c8233
Compare
d80b481
into
data-integrations:release/1.2
[🍒]
🍒 [cherrypick]
PR : (#151)
Description:
Route all ServiceNow API calls (table reads, schema fetch, batch sink writes, OAuth token requests) through an optional HTTP proxy, modeled on the http-plugins proxy support.
Add proxyUrl, proxyUsername, proxyPassword properties to ServiceNowConnectorConfig (nullable, macro-enabled) with validation
Build a proxy-aware Apache HttpClient in RestAPIClient used by all GET/POST/DELETE calls
Route the OAuth token call through the proxy via a new ProxyOltuHttpClient (Oltu HttpClient backed by Apache HttpClient); falls back to URLConnectionClient when no proxy is configured
Add Proxy fields and a "Proxy authentication" filter to the connector, source, multi-source and sink widgets
Document the new properties and add unit tests for proxy config
ProxyOltuHttpClientTest: covers the OAuth-over-proxy client (POST token parsing, GET path, IOException wrapped as OAuthSystemException)
RestAPIClientProxyTest: covers getHttpClientBuilder() proxy wiring (proxy host + credentials, proxy-only, no-proxy, invalid URL) by inspecting the real HttpClientBuilder's fields, since its setters are final