/** * 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.box.internal; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import com.box.sdk.BoxAPIConnection; import com.box.sdk.BoxAPIException; import com.box.sdk.BoxDeveloperEditionAPIConnection; import com.box.sdk.IAccessTokenCache; import com.box.sdk.InMemoryLRUAccessTokenCache; import com.box.sdk.JWTEncryptionPreferences; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.ProxyConfig; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebClientOptions; import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlDivision; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput; import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput; import com.gargoylesoftware.htmlunit.html.HtmlTextInput; import com.gargoylesoftware.htmlunit.util.WebConnectionWrapper; import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.box.BoxConfiguration; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.jsse.SSLContextParameters; import org.apache.http.HttpHeaders; import org.apache.http.HttpHost; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * BoxConnectionHelper * * <p> * Utility class for creating Box API Connections */ public final class BoxConnectionHelper { private static final Logger LOG = LoggerFactory.getLogger(BoxConnectionHelper.class); private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("&?([^=]+)=([^&]+)"); private BoxConnectionHelper() { // hide utility class constructor } public static BoxAPIConnection createConnection(final BoxConfiguration configuration) { if (configuration.getAuthenticationType() == null) { throw new RuntimeCamelException( "Box API connection failed: Authentication type not specified in configuration"); } switch (configuration.getAuthenticationType()) { case BoxConfiguration.APP_ENTERPRISE_AUTHENTICATION: return createAppEnterpriseAuthenticatedConnection(configuration); case BoxConfiguration.APP_USER_AUTHENTICATION: return createAppUserAuthenticatedConnection(configuration); case BoxConfiguration.STANDARD_AUTHENTICATION: return createStandardAuthenticatedConnection(configuration); default: throw new RuntimeCamelException(String.format("Box API connection failed: Invalid authentication type '%s'", configuration.getAuthenticationType())); } } public static BoxAPIConnection createStandardAuthenticatedConnection(BoxConfiguration configuration) { // Create web client for first leg of OAuth2 // final WebClient webClient = new WebClient(); final WebClientOptions options = webClient.getOptions(); options.setRedirectEnabled(true); options.setJavaScriptEnabled(false); options.setThrowExceptionOnFailingStatusCode(true); options.setThrowExceptionOnScriptError(true); options.setPrintContentOnFailingStatusCode(LOG.isDebugEnabled()); try { // use default SSP to create supported non-SSL protocols list final SSLContext sslContext = new SSLContextParameters().createSSLContext(null); options.setSSLClientProtocols(sslContext.createSSLEngine().getEnabledProtocols()); } catch (GeneralSecurityException e) { throw ObjectHelper.wrapRuntimeCamelException(e); } catch (IOException e) { throw ObjectHelper.wrapRuntimeCamelException(e); } finally { if (webClient != null) { webClient.close(); } } // disable default gzip compression, as htmlunit does not negotiate // pages sent with no compression new WebConnectionWrapper(webClient) { @Override public WebResponse getResponse(WebRequest request) throws IOException { request.setAdditionalHeader(HttpHeaders.ACCEPT_ENCODING, "identity"); return super.getResponse(request); } }; // add HTTP proxy if set final Map<String, Object> httpParams = configuration.getHttpParams(); if (httpParams != null && httpParams.get("http.route.default-proxy") != null) { final HttpHost proxyHost = (HttpHost) httpParams.get("http.route.default-proxy"); final Boolean socksProxy = (Boolean) httpParams.get("http.route.socks-proxy"); final ProxyConfig proxyConfig = new ProxyConfig(proxyHost.getHostName(), proxyHost.getPort(), socksProxy != null ? socksProxy : false); options.setProxyConfig(proxyConfig); } // authorize application on user's behalf try { // generate anti-forgery token to prevent/detect CSRF attack final String csrfToken = String.valueOf(new SecureRandom().nextLong()); final HtmlPage authPage = webClient.getPage(authorizationUrl(configuration.getClientId(), csrfToken)); // look for <div role="error_message"> final HtmlDivision div = authPage .getFirstByXPath("//div[contains(concat(' ', @class, ' '), ' error_message ')]"); if (div != null) { final String errorMessage = div.getTextContent().replaceAll("\\s+", " ") .replaceAll(" Show Error Details", ":").trim(); throw new IllegalArgumentException("Error authorizing application: " + errorMessage); } // submit login credentials final HtmlForm loginForm = authPage.getFormByName("login_form"); final HtmlTextInput login = loginForm.getInputByName("login"); login.setText(configuration.getUserName()); final HtmlPasswordInput password = loginForm.getInputByName("password"); password.setText(configuration.getUserPassword()); final HtmlSubmitInput submitInput = loginForm.getInputByName("login_submit"); // submit consent final HtmlPage consentPage = submitInput.click(); final HtmlForm consentForm = consentPage.getFormByName("consent_form"); final HtmlButton consentAccept = consentForm.getButtonByName("consent_accept"); // disable redirect to avoid loading redirect URL webClient.getOptions().setRedirectEnabled(false); // validate CSRF and get authorization code String redirectQuery; try { final Page redirectPage = consentAccept.click(); redirectQuery = redirectPage.getUrl().getQuery(); } catch (FailingHttpStatusCodeException e) { // escalate non redirect errors if (e.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { throw e; } final String location = e.getResponse().getResponseHeaderValue("Location"); redirectQuery = new URL(location).getQuery(); } final Map<String, String> params = new HashMap<String, String>(); final Matcher matcher = QUERY_PARAM_PATTERN.matcher(redirectQuery); while (matcher.find()) { params.put(matcher.group(1), matcher.group(2)); } final String state = params.get("state"); if (!csrfToken.equals(state)) { throw new SecurityException("Invalid CSRF code!"); } else { // get authorization code final String authorizationCode = params.get("code"); return new BoxAPIConnection(configuration.getClientId(), configuration.getClientSecret(), authorizationCode); } } catch (BoxAPIException e) { throw new RuntimeCamelException( String.format("Box API connection failed: API returned the error code %d\n\n%s", e.getResponseCode(), e.getResponse()), e); } catch (Exception e) { throw new RuntimeCamelException(String.format("Box API connection failed: %s", e.getMessage()), e); } } public static BoxAPIConnection createAppUserAuthenticatedConnection(BoxConfiguration configuration) { // Create Encryption Preferences JWTEncryptionPreferences encryptionPref = new JWTEncryptionPreferences(); encryptionPref.setPublicKeyID(configuration.getPublicKeyId()); try { encryptionPref.setPrivateKey(new String(Files.readAllBytes(Paths.get(configuration.getPrivateKeyFile())))); } catch (Exception e) { throw new RuntimeCamelException("Box API connection failed: could not read privateKeyFile", e); } encryptionPref.setPrivateKeyPassword(configuration.getPrivateKeyPassword()); encryptionPref.setEncryptionAlgorithm(configuration.getEncryptionAlgorithm()); IAccessTokenCache accessTokenCache = configuration.getAccessTokenCache(); if (accessTokenCache == null) { accessTokenCache = new InMemoryLRUAccessTokenCache(configuration.getMaxCacheEntries()); } try { return BoxDeveloperEditionAPIConnection.getAppUserConnection(configuration.getUserId(), configuration.getClientId(), configuration.getClientSecret(), encryptionPref, accessTokenCache); } catch (BoxAPIException e) { throw new RuntimeCamelException( String.format("Box API connection failed: API returned the error code %d\n\n%s", e.getResponseCode(), e.getResponse()), e); } } public static BoxAPIConnection createAppEnterpriseAuthenticatedConnection(BoxConfiguration configuration) { // Create Encryption Preferences JWTEncryptionPreferences encryptionPref = new JWTEncryptionPreferences(); encryptionPref.setPublicKeyID(configuration.getPublicKeyId()); try { encryptionPref.setPrivateKey(new String(Files.readAllBytes(Paths.get(configuration.getPrivateKeyFile())))); } catch (Exception e) { throw new RuntimeCamelException("Box API connection failed: could not read privateKeyFile", e); } encryptionPref.setPrivateKeyPassword(configuration.getPrivateKeyPassword()); encryptionPref.setEncryptionAlgorithm(configuration.getEncryptionAlgorithm()); IAccessTokenCache accessTokenCache = configuration.getAccessTokenCache(); if (accessTokenCache == null) { accessTokenCache = new InMemoryLRUAccessTokenCache(configuration.getMaxCacheEntries()); } try { return BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(configuration.getEnterpriseId(), configuration.getClientId(), configuration.getClientSecret(), encryptionPref, accessTokenCache); } catch (BoxAPIException e) { throw new RuntimeCamelException( String.format("Box API connection failed: API returned the error code %d\n\n%s", e.getResponseCode(), e.getResponse()), e); } } public static String authorizationUrl(String clientId, String stateToken) { return "https://account.box.com/api/oauth2/authorize?response_type=code&redirect_url=https%3A%2F%2Flocalhost%2F&client_id=" + clientId + "&state=" + stateToken; } }