/*
* Copyright 2017 StreamSets Inc.
*
* Licensed under 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 com.streamsets.pipeline.lib.http;
import com.streamsets.pipeline.api.Record;
import com.streamsets.pipeline.api.Stage;
import com.streamsets.pipeline.api.StageException;
import com.streamsets.pipeline.api.el.ELEval;
import com.streamsets.pipeline.api.el.ELEvalException;
import com.streamsets.pipeline.api.el.ELVars;
import com.streamsets.pipeline.api.impl.Utils;
import com.streamsets.pipeline.lib.el.RecordEL;
import com.streamsets.pipeline.lib.el.VaultEL;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.filter.EncodingFilter;
import org.glassfish.jersey.client.oauth1.AccessToken;
import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider;
import org.glassfish.jersey.message.GZipEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.streamsets.pipeline.lib.http.Errors.*;
public class HttpClientCommon {
private static final Logger LOG = LoggerFactory.getLogger(HttpClientCommon.class);
private static final String RESOURCE_CONFIG_NAME = "resourceUrl";
private static final String HTTP_METHOD_CONFIG_NAME = "httpMethod";
private static final String HEADER_CONFIG_NAME = "headers";
public static final String DATA_FORMAT_CONFIG_PREFIX = "conf.dataFormatConfig.";
private static final String SSL_CONFIG_PREFIX = "conf.tlsConfig.";
private static final String VAULT_EL_PREFIX = VaultEL.PREFIX + ":read";
private static final String OAUTH2_GROUP = "OAUTH2";
private static final String CONF_CLIENT_OAUTH2_TOKEN_URL = "conf.client.oauth2.tokenUrl";
private final JerseyClientConfigBean jerseyClientConfig;
private AccessToken authToken;
private Client client = null;
private ELVars resourceVars;
private ELVars methodVars;
private ELVars headerVars;
private ELEval resourceEval;
private ELEval methodEval;
private ELEval headerEval;
public HttpClientCommon(JerseyClientConfigBean jerseyClientConfig) {
this.jerseyClientConfig = jerseyClientConfig;
}
public List<Stage.ConfigIssue> init(List<Stage.ConfigIssue> issues, Stage.Context context) {
if (jerseyClientConfig.tlsConfig.isEitherStoreEnabled()) {
jerseyClientConfig.tlsConfig.init(
context,
Groups.TLS.name(),
SSL_CONFIG_PREFIX,
issues
);
}
resourceVars = context.createELVars();
resourceEval = context.createELEval(RESOURCE_CONFIG_NAME);
methodVars = context.createELVars();
methodEval = context.createELEval(HTTP_METHOD_CONFIG_NAME);
headerVars = context.createELVars();
headerEval = context.createELEval(HEADER_CONFIG_NAME);
jerseyClientConfig.init(context, Groups.PROXY.name(), "conf.client.", issues);
// Validation succeeded so configure the client.
if (issues.isEmpty()) {
ClientConfig clientConfig = new ClientConfig()
.property(ClientProperties.CONNECT_TIMEOUT, jerseyClientConfig.connectTimeoutMillis)
.property(ClientProperties.READ_TIMEOUT, jerseyClientConfig.readTimeoutMillis)
.property(ClientProperties.ASYNC_THREADPOOL_SIZE, jerseyClientConfig.numThreads)
.property(ClientProperties.REQUEST_ENTITY_PROCESSING, jerseyClientConfig.transferEncoding)
.property(ClientProperties.USE_ENCODING, jerseyClientConfig.httpCompression.getValue())
.connectorProvider(new GrizzlyConnectorProvider(new GrizzlyClientCustomizer(jerseyClientConfig)));
ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig);
configureCompression(clientBuilder);
if (jerseyClientConfig.useProxy) {
JerseyClientUtil.configureProxy(jerseyClientConfig.proxy, clientBuilder);
}
JerseyClientUtil.configureSslContext(jerseyClientConfig.tlsConfig, clientBuilder);
configureAuthAndBuildClient(clientBuilder, issues, context);
}
return issues;
}
private void configureCompression(ClientBuilder clientBuilder) {
if (jerseyClientConfig.httpCompression != null) {
switch (jerseyClientConfig.httpCompression) {
case SNAPPY:
clientBuilder.register(SnappyEncoder.class);
break;
case GZIP:
clientBuilder.register(GZipEncoder.class);
break;
case NONE:
default:
break;
}
clientBuilder.register(EncodingFilter.class);
}
}
/**
* Helper to apply authentication properties to Jersey client.
*
* @param clientBuilder Jersey Client builder to configure
*/
private void configureAuthAndBuildClient(
ClientBuilder clientBuilder,
List<Stage.ConfigIssue> issues,
Stage.Context context
) {
if (jerseyClientConfig.authType == AuthenticationType.OAUTH) {
authToken = JerseyClientUtil.configureOAuth1(jerseyClientConfig.oauth, clientBuilder);
} else if (jerseyClientConfig.authType != AuthenticationType.NONE) {
JerseyClientUtil.configurePasswordAuth(jerseyClientConfig.authType, jerseyClientConfig.basicAuth, clientBuilder);
}
client = clientBuilder.build();
if (jerseyClientConfig.useOAuth2) {
try {
jerseyClientConfig.oauth2.init(context, issues, client);
} catch (AuthenticationFailureException ex) {
LOG.error("OAuth2 Authentication failed", ex);
issues.add(context.createConfigIssue(OAUTH2_GROUP, CONF_CLIENT_OAUTH2_TOKEN_URL, HTTP_21));
} catch (IOException ex) {
LOG.error("OAuth2 Authentication Response does not contain access token", ex);
issues.add(context.createConfigIssue(OAUTH2_GROUP, CONF_CLIENT_OAUTH2_TOKEN_URL, HTTP_22));
} catch (NotFoundException ex) {
LOG.error(Utils.format(
HTTP_24.getMessage(), jerseyClientConfig.oauth2.tokenUrl, jerseyClientConfig.oauth2.transferEncoding), ex);
issues.add(context.createConfigIssue(OAUTH2_GROUP, CONF_CLIENT_OAUTH2_TOKEN_URL, HTTP_24,
jerseyClientConfig.oauth2.tokenUrl, jerseyClientConfig.oauth2.transferEncoding));
}
}
}
/**
* Returns true if the request contains potentially sensitive information such as a vault:read EL.
*
* @return whether or not the request had sensitive information detected.
*/
public boolean requestContainsSensitiveInfo(Map<String, String> headers, String requestBody) {
boolean sensitive = false;
for (Map.Entry<String, String> header : headers.entrySet()) {
if (header.getKey().contains(VAULT_EL_PREFIX) || header.getValue().contains(VAULT_EL_PREFIX)) {
sensitive = true;
break;
}
}
if (requestBody != null && requestBody.contains(VAULT_EL_PREFIX)) {
sensitive = true;
}
return sensitive;
}
/**
* Evaluates any EL expressions in the headers section of the stage configuration.
*
* @param record current record in context for EL evaluation
* @return Map of headers that can be added to the Jersey Client request
* @throws StageException if an expression could not be evaluated
*/
public MultivaluedMap<String, Object> resolveHeaders(
Map<String, String> headers,
Record record
) throws StageException {
RecordEL.setRecordInContext(headerVars, record);
MultivaluedMap<String, Object> requestHeaders = new MultivaluedHashMap<>();
for (Map.Entry<String, String> entry : headers.entrySet()) {
List<Object> header = new ArrayList<>(1);
Object resolvedValue = headerEval.eval(headerVars, entry.getValue(), String.class);
header.add(resolvedValue);
requestHeaders.put(entry.getKey(), header);
}
return requestHeaders;
}
/**
* Determines the HTTP method to use for the next request. It may include an EL expression to evaluate.
*
* @param record Current record to set in context.
* @return the {@link HttpMethod} to use for the request
* @throws ELEvalException if an expression is supplied that cannot be evaluated
*/
public HttpMethod getHttpMethod(
HttpMethod httpMethod,
String methodExpression,
Record record
) throws ELEvalException {
if (httpMethod != HttpMethod.EXPRESSION) {
return httpMethod;
}
RecordEL.setRecordInContext(methodVars, record);
return HttpMethod.valueOf(methodEval.eval(methodVars, methodExpression, String.class));
}
public Client getClient() {
return client;
}
public AccessToken getAuthToken() {
return authToken;
}
public String getResolvedUrl(String resourceUrl, Record record) throws ELEvalException {
RecordEL.setRecordInContext(resourceVars, record);
return resourceEval.eval(resourceVars, resourceUrl, String.class);
}
public void destroy() {
if (client != null) {
client.close();
}
}
}