/** * 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.camel.component.salesforce; import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.apache.camel.CamelContext; import org.apache.camel.ComponentVerifier; import org.apache.camel.Endpoint; import org.apache.camel.SSLContextParametersAware; import org.apache.camel.VerifiableComponent; import org.apache.camel.component.salesforce.api.SalesforceException; import org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase; import org.apache.camel.component.salesforce.internal.OperationName; import org.apache.camel.component.salesforce.internal.SalesforceSession; import org.apache.camel.component.salesforce.internal.streaming.SubscriptionHelper; import org.apache.camel.impl.DefaultComponent; import org.apache.camel.spi.Metadata; import org.apache.camel.util.IntrospectionSupport; import org.apache.camel.util.ServiceHelper; import org.apache.camel.util.StringHelper; import org.apache.camel.util.jsse.KeyStoreParameters; import org.apache.camel.util.jsse.SSLContextParameters; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.ProxyConfiguration; import org.eclipse.jetty.client.Socks4Proxy; import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.DigestAuthentication; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.camel.component.salesforce.SalesforceLoginConfig.DEFAULT_LOGIN_URL; /** * Represents the component that manages {@link SalesforceEndpoint}. */ @Metadata(label = "verifiers", enums = "parameters,connectivity") public class SalesforceComponent extends DefaultComponent implements VerifiableComponent, SSLContextParametersAware { static final int CONNECTION_TIMEOUT = 60000; static final Pattern SOBJECT_NAME_PATTERN = Pattern.compile("^.*[\\?&]sObjectName=([^&,]+).*$"); static final String APEX_CALL_PREFIX = OperationName.APEX_CALL.value() + "/"; private static final Logger LOG = LoggerFactory.getLogger(SalesforceComponent.class); @Metadata(description = "All authentication configuration in one nested bean, all properties set there can be set" + " directly on the component as well", label = "common,security") private SalesforceLoginConfig loginConfig; @Metadata(description = "URL of the Salesforce instance used after authantication, by default received from" + " Salesforce on successful authentication", label = "common,security") private String instanceUrl; // allow fine grained login as well @Metadata(description = "URL of the Salesforce instance used for authentication, by default set to " + DEFAULT_LOGIN_URL, label = "common,security", defaultValue = DEFAULT_LOGIN_URL, required = "true") private String loginUrl; @Metadata(description = "OAuth Consumer Key of the connected app configured in the Salesforce instance setup." + " Typically a connected app needs to be configured but one can be provided by installing a package.", label = "common,security", secret = true, required = "true") private String clientId; @Metadata(description = "OAuth Consumer Secret of the connected app configured in the Salesforce instance setup.", label = "common,security", secret = true) private String clientSecret; @Metadata(description = "Refresh token already obtained in the refresh token OAuth flow. One needs to setup a web" + " application and configure a callback URL to receive the refresh token, or configure using the builtin" + " callback at https://login.salesforce.com/services/oauth2/success or " + " https://test.salesforce.com/services/oauth2/success and then retrive the refresh_token from the URL at the" + " end of the flow. Note that in development organizations Salesforce allows hosting the callback web " + " application at localhost.", label = "common,security", secret = true) private String refreshToken; @Metadata(description = "Username used in OAuth flow to gain access to access token. It's easy to get started with" + " password OAuth flow, but in general one should avoid it as it is deemed less secure than other flows.", label = "common,security", secret = true) private String userName; @Metadata(description = "Password used in OAuth flow to gain access to access token. It's easy to get started with" + " password OAuth flow, but in general one should avoid it as it is deemed less secure than other flows." + " Make sure that you append security token to the end of the password if using one.", label = "common,security", secret = true) private String password; @Metadata(description = "KeyStore parameters to use in OAuth JWT flow. The KeyStore should contain only one entry" + " with private key and certificate. Salesforce does not verify the certificate chain, so this can easily be" + " a selfsigned certificate. Make sure that you upload the certificate to the corresponding connected app.", label = "common,security", secret = true) private KeyStoreParameters keystore; @Metadata(description = "Explicit authentication method to be used, one of USERNAME_PASSWORD, REFRESH_TOKEN or JWT." + " Salesforce component can auto-determine the authentication method to use from the properties set, set this " + " property to eliminate any ambiguity.", label = "common,security", secret = false, enums = "USERNAME_PASSWORD,REFRESH_TOKEN,JWT") private AuthenticationType authenticationType; @Metadata(description = "If set to true prevents the component from authenticating to Salesforce with the start of" + " the component. You would generaly set this to the (default) false and authenticate early and be immediately" + " aware of any authentication issues.", defaultValue = "false", label = "common,security") private boolean lazyLogin; @Metadata(description = "Global endpoint configuration - use to set values that are common to all endpoints", label = "common,advanced") private SalesforceEndpointConfig config; @Metadata(description = "Used to set any properties that can be configured on the underlying HTTP client. Have a" + " look at properties of SalesforceHttpClient and the Jetty HttpClient for all available options.", label = "common,advanced") private Map<String, Object> httpClientProperties; @Metadata(description = "SSL parameters to use, see SSLContextParameters class for all available options.", label = "common,security") private SSLContextParameters sslContextParameters; @Metadata(description = "Enable usage of global SSL context parameters", label = "security", defaultValue = "false") private boolean useGlobalSslContextParameters; // Proxy host and port @Metadata(description = "Hostname of the HTTP proxy server to use.", label = "common,proxy") private String httpProxyHost; @Metadata(description = "Port number of the HTTP proxy server to use.", label = "common,proxy") private Integer httpProxyPort; @Metadata(description = "If set to true the configures the HTTP proxy to use as a SOCKS4 proxy.", defaultValue = "false", label = "common,proxy") private boolean isHttpProxySocks4; @Metadata(description = "If set to false disables the use of TLS when accessing the HTTP proxy.", defaultValue = "true", label = "common,proxy,security") private boolean isHttpProxySecure = true; @Metadata(description = "A list of addresses for which HTTP proxy server should be used.", label = "common,proxy") private Set<String> httpProxyIncludedAddresses; @Metadata(description = "A list of addresses for which HTTP proxy server should not be used.", label = "common,proxy") private Set<String> httpProxyExcludedAddresses; // Proxy basic authentication @Metadata(description = "Username to use to authenticate against the HTTP proxy server.", label = "common,proxy,security", secret = true) private String httpProxyUsername; @Metadata(description = "Password to use to authenticate against the HTTP proxy server.", label = "common,proxy,security", secret = true) private String httpProxyPassword; @Metadata(description = "Used in authentication against the HTTP proxy server, needs to match the URI of the proxy" + " server in order for the httpProxyUsername and httpProxyPassword to be used for authentication.", label = "common,proxy,security") private String httpProxyAuthUri; @Metadata(description = "Realm of the proxy server, used in preemptive Basic/Digest authentication methods against" + " the HTTP proxy server.", label = "common,proxy,security") private String httpProxyRealm; @Metadata(description = "If set to true Digest authentication will be used when authenticating to the HTTP proxy," + "otherwise Basic authorization method will be used", defaultValue = "false", label = "common,proxy,security") private boolean httpProxyUseDigestAuth; @Metadata(description = "In what packages are the generated DTO classes. Typically the classes would be generated" + " using camel-salesforce-maven-plugin. Set it if using the generated DTOs to gain the benefit of using short " + " SObject names in parameters/header values.", label = "common") private String[] packages; // component state private SalesforceHttpClient httpClient; private SalesforceSession session; private Map<String, Class<?>> classMap; // Lazily created helper for consumer endpoints private SubscriptionHelper subscriptionHelper; public SalesforceComponent() { } public SalesforceComponent(CamelContext context) { super(context); } protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { // get Operation from remaining URI OperationName operationName = null; String topicName = null; String apexUrl = null; try { LOG.debug("Creating endpoint for: {}", remaining); if (remaining.startsWith(APEX_CALL_PREFIX)) { // extract APEX URL apexUrl = remaining.substring(APEX_CALL_PREFIX.length()); remaining = OperationName.APEX_CALL.value(); } operationName = OperationName.fromValue(remaining); } catch (IllegalArgumentException ex) { // if its not an operation name, treat is as topic name for consumer endpoints topicName = remaining; } // create endpoint config if (config == null) { config = new SalesforceEndpointConfig(); } if (config.getHttpClient() == null) { // set the component's httpClient as default config.setHttpClient(httpClient); } // create a deep copy and map parameters final SalesforceEndpointConfig copy = config.copy(); setProperties(copy, parameters); // set apexUrl in endpoint config if (apexUrl != null) { copy.setApexUrl(apexUrl); } final SalesforceEndpoint endpoint = new SalesforceEndpoint(uri, this, copy, operationName, topicName); // map remaining parameters to endpoint (specifically, synchronous) setProperties(endpoint, parameters); // if operation is APEX call, map remaining parameters to query params if (operationName == OperationName.APEX_CALL && !parameters.isEmpty()) { Map<String, Object> queryParams = new HashMap<String, Object>(copy.getApexQueryParams()); // override component params with endpoint params queryParams.putAll(parameters); parameters.clear(); copy.setApexQueryParams(queryParams); } return endpoint; } private Map<String, Class<?>> parsePackages() { Map<String, Class<?>> result = new HashMap<String, Class<?>>(); Set<Class<?>> classes = getCamelContext().getPackageScanClassResolver(). findImplementations(AbstractSObjectBase.class, packages); for (Class<?> aClass : classes) { // findImplementations also returns AbstractSObjectBase for some reason!!! if (AbstractSObjectBase.class != aClass) { result.put(aClass.getSimpleName(), aClass); } } return result; } @Override protected void doStart() throws Exception { if (loginConfig == null) { loginConfig = new SalesforceLoginConfig(); loginConfig.setInstanceUrl(instanceUrl); loginConfig.setClientId(clientId); loginConfig.setClientSecret(clientSecret); loginConfig.setKeystore(keystore); loginConfig.setLazyLogin(lazyLogin); loginConfig.setLoginUrl(loginUrl); loginConfig.setPassword(password); loginConfig.setRefreshToken(refreshToken); loginConfig.setType(authenticationType); loginConfig.setUserName(userName); LOG.debug("Created login configuration: {}", loginConfig); } else { LOG.debug("Using shared login configuration: {}", loginConfig); } // create a Jetty HttpClient if not already set if (null == httpClient) { if (config != null && config.getHttpClient() != null) { httpClient = config.getHttpClient(); } else { // set ssl context parameters if set SSLContextParameters contextParameters = sslContextParameters; if (contextParameters == null) { contextParameters = retrieveGlobalSslContextParameters(); } if (contextParameters == null) { contextParameters = new SSLContextParameters(); } final SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setSslContext(contextParameters.createSSLContext(getCamelContext())); httpClient = new SalesforceHttpClient(sslContextFactory); // default settings, use httpClientProperties to set other properties httpClient.setConnectTimeout(CONNECTION_TIMEOUT); } } // set HTTP client parameters if (httpClientProperties != null && !httpClientProperties.isEmpty()) { IntrospectionSupport.setProperties(getCamelContext().getTypeConverter(), httpClient, new HashMap<String, Object>(httpClientProperties)); } // set HTTP proxy settings if (this.httpProxyHost != null && httpProxyPort != null) { Origin.Address proxyAddress = new Origin.Address(this.httpProxyHost, this.httpProxyPort); ProxyConfiguration.Proxy proxy; if (isHttpProxySocks4) { proxy = new Socks4Proxy(proxyAddress, isHttpProxySecure); } else { proxy = new HttpProxy(proxyAddress, isHttpProxySecure); } if (httpProxyIncludedAddresses != null && !httpProxyIncludedAddresses.isEmpty()) { proxy.getIncludedAddresses().addAll(httpProxyIncludedAddresses); } if (httpProxyExcludedAddresses != null && !httpProxyExcludedAddresses.isEmpty()) { proxy.getExcludedAddresses().addAll(httpProxyExcludedAddresses); } httpClient.getProxyConfiguration().getProxies().add(proxy); } if (this.httpProxyUsername != null && httpProxyPassword != null) { StringHelper.notEmpty(httpProxyAuthUri, "httpProxyAuthUri"); StringHelper.notEmpty(httpProxyRealm, "httpProxyRealm"); final Authentication authentication; if (httpProxyUseDigestAuth) { authentication = new DigestAuthentication(new URI(httpProxyAuthUri), httpProxyRealm, httpProxyUsername, httpProxyPassword); } else { authentication = new BasicAuthentication(new URI(httpProxyAuthUri), httpProxyRealm, httpProxyUsername, httpProxyPassword); } httpClient.getAuthenticationStore().addAuthentication(authentication); } // support restarts if (this.session == null) { this.session = new SalesforceSession(getCamelContext(), httpClient, httpClient.getTimeout(), loginConfig); } // set session before calling start() httpClient.setSession(this.session); // start the Jetty client to initialize thread pool, etc. httpClient.start(); // login at startup if lazyLogin is disabled if (!loginConfig.isLazyLogin()) { ServiceHelper.startService(session); } if (packages != null && packages.length > 0) { // parse the packages to create SObject name to class map classMap = parsePackages(); LOG.info("Found {} generated classes in packages: {}", classMap.size(), Arrays.asList(packages)); } else { // use an empty map to avoid NPEs later LOG.warn("Missing property packages, getSObject* operations will NOT work"); classMap = new HashMap<String, Class<?>>(0); } if (subscriptionHelper != null) { ServiceHelper.startService(subscriptionHelper); } } @Override protected void doStop() throws Exception { if (classMap != null) { classMap.clear(); } try { if (subscriptionHelper != null) { // shutdown all streaming connections // note that this is done in the component, and not in consumer ServiceHelper.stopService(subscriptionHelper); subscriptionHelper = null; } if (session != null && session.getAccessToken() != null) { try { // logout of Salesforce ServiceHelper.stopService(session); } catch (SalesforceException ignored) { } } } finally { if (httpClient != null) { // shutdown http client connections httpClient.stop(); // destroy http client if it was created by the component if (config.getHttpClient() == null) { httpClient.destroy(); } httpClient = null; } } } public SubscriptionHelper getSubscriptionHelper() throws Exception { if (subscriptionHelper == null) { // lazily create subscription helper subscriptionHelper = new SubscriptionHelper(this); // also start the helper to connect to Salesforce ServiceHelper.startService(subscriptionHelper); } return subscriptionHelper; } public AuthenticationType getAuthenticationType() { return authenticationType; } public void setAuthenticationType(AuthenticationType authenticationType) { this.authenticationType = authenticationType; } public SalesforceLoginConfig getLoginConfig() { return loginConfig; } public void setLoginConfig(SalesforceLoginConfig loginConfig) { this.loginConfig = loginConfig; } public void setInstanceUrl(String instanceUrl) { this.instanceUrl = instanceUrl; } public void setLoginUrl(String loginUrl) { this.loginUrl = loginUrl; } public String getClientId() { return clientId; } public void setClientId(String clientId) { this.clientId = clientId; } public String getClientSecret() { return clientSecret; } public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } public void setKeystore(final KeyStoreParameters keystore) { this.keystore = keystore; } public KeyStoreParameters getKeystore() { return keystore; } public String getRefreshToken() { return refreshToken; } public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isLazyLogin() { return lazyLogin; } public void setLazyLogin(boolean lazyLogin) { this.lazyLogin = lazyLogin; } public SalesforceEndpointConfig getConfig() { return config; } public void setConfig(SalesforceEndpointConfig config) { this.config = config; } public Map<String, Object> getHttpClientProperties() { return httpClientProperties; } public void setHttpClientProperties(Map<String, Object> httpClientProperties) { this.httpClientProperties = httpClientProperties; } public SSLContextParameters getSslContextParameters() { return sslContextParameters; } public void setSslContextParameters(SSLContextParameters sslContextParameters) { this.sslContextParameters = sslContextParameters; } @Override public boolean isUseGlobalSslContextParameters() { return this.useGlobalSslContextParameters; } @Override public void setUseGlobalSslContextParameters(boolean useGlobalSslContextParameters) { this.useGlobalSslContextParameters = useGlobalSslContextParameters; } public String getHttpProxyHost() { return httpProxyHost; } public void setHttpProxyHost(String httpProxyHost) { this.httpProxyHost = httpProxyHost; } public Integer getHttpProxyPort() { return httpProxyPort; } public void setHttpProxyPort(Integer httpProxyPort) { this.httpProxyPort = httpProxyPort; } public String getHttpProxyUsername() { return httpProxyUsername; } public void setHttpProxyUsername(String httpProxyUsername) { this.httpProxyUsername = httpProxyUsername; } public String getHttpProxyPassword() { return httpProxyPassword; } public void setHttpProxyPassword(String httpProxyPassword) { this.httpProxyPassword = httpProxyPassword; } public boolean isHttpProxySocks4() { return isHttpProxySocks4; } public void setIsHttpProxySocks4(boolean isHttpProxySocks4) { this.isHttpProxySocks4 = isHttpProxySocks4; } public boolean isHttpProxySecure() { return isHttpProxySecure; } public void setIsHttpProxySecure(boolean isHttpProxySecure) { this.isHttpProxySecure = isHttpProxySecure; } public Set<String> getHttpProxyIncludedAddresses() { return httpProxyIncludedAddresses; } public void setHttpProxyIncludedAddresses(Set<String> httpProxyIncludedAddresses) { this.httpProxyIncludedAddresses = httpProxyIncludedAddresses; } public Set<String> getHttpProxyExcludedAddresses() { return httpProxyExcludedAddresses; } public void setHttpProxyExcludedAddresses(Set<String> httpProxyExcludedAddresses) { this.httpProxyExcludedAddresses = httpProxyExcludedAddresses; } public String getHttpProxyAuthUri() { return httpProxyAuthUri; } public void setHttpProxyAuthUri(String httpProxyAuthUri) { this.httpProxyAuthUri = httpProxyAuthUri; } public String getHttpProxyRealm() { return httpProxyRealm; } public void setHttpProxyRealm(String httpProxyRealm) { this.httpProxyRealm = httpProxyRealm; } public boolean isHttpProxyUseDigestAuth() { return httpProxyUseDigestAuth; } public void setHttpProxyUseDigestAuth(boolean httpProxyUseDigestAuth) { this.httpProxyUseDigestAuth = httpProxyUseDigestAuth; } public String[] getPackages() { return packages; } public void setPackages(String[] packages) { this.packages = packages; } public void setPackages(String packages) { // split using comma if (packages != null) { setPackages(packages.split(",")); } } public SalesforceSession getSession() { return session; } public Map<String, Class<?>> getClassMap() { return classMap; } public ComponentVerifier getVerifier() { return new SalesforceComponentVerifier(this); } }