/** * 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.internal.client; import java.io.InputStream; import org.apache.camel.component.salesforce.SalesforceHttpClient; import org.apache.camel.component.salesforce.api.SalesforceException; import org.apache.camel.component.salesforce.internal.SalesforceSession; import org.eclipse.jetty.client.HttpContentResponse; import org.eclipse.jetty.client.HttpConversation; import org.eclipse.jetty.client.ProtocolHandler; import org.eclipse.jetty.client.ResponseNotifier; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SalesforceSecurityHandler implements ProtocolHandler { static final String CLIENT_ATTRIBUTE = SalesforceSecurityHandler.class.getName().concat("camel-salesforce-client"); static final String AUTHENTICATION_REQUEST_ATTRIBUTE = SalesforceSecurityHandler.class.getName().concat(".request"); private static final Logger LOG = LoggerFactory.getLogger(SalesforceSecurityHandler.class); private static final String AUTHENTICATION_RETRIES_ATTRIBUTE = SalesforceSecurityHandler.class.getName().concat(".retries"); private final SalesforceHttpClient httpClient; private final SalesforceSession session; private final int maxAuthenticationRetries; private final int maxContentLength; private final ResponseNotifier notifier; public SalesforceSecurityHandler(SalesforceHttpClient httpClient) { this.httpClient = httpClient; this.session = httpClient.getSession(); this.maxAuthenticationRetries = httpClient.getMaxRetries(); this.maxContentLength = httpClient.getMaxContentLength(); this.notifier = new ResponseNotifier(); } @Override public boolean accept(Request request, Response response) { HttpConversation conversation = ((SalesforceHttpRequest) request).getConversation(); Integer retries = (Integer) conversation.getAttribute(AUTHENTICATION_RETRIES_ATTRIBUTE); // is this an authentication response for a previously handled conversation? if (conversation.getAttribute(AUTHENTICATION_REQUEST_ATTRIBUTE) != null && (retries == null || retries <= maxAuthenticationRetries)) { return true; } final int status = response.getStatus(); // handle UNAUTHORIZED and BAD_REQUEST for Bulk API, // the actual InvalidSessionId Bulk API error is checked and handled in the listener // also check retries haven't exceeded maxAuthenticationRetries return (status == HttpStatus.UNAUTHORIZED_401 || status == HttpStatus.BAD_REQUEST_400) && (retries == null || retries <= maxAuthenticationRetries); } @Override public Response.Listener getResponseListener() { return new SecurityListener(maxContentLength); } private class SecurityListener extends BufferingResponseListener { SecurityListener(int maxLength) { super(maxLength); } @Override public void onComplete(Result result) { SalesforceHttpRequest request = (SalesforceHttpRequest)result.getRequest(); ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding()); // get number of retries HttpConversation conversation = request.getConversation(); Integer retries = (Integer) conversation.getAttribute(AUTHENTICATION_RETRIES_ATTRIBUTE); if (retries == null) { retries = 0; } // get AbstractClientBase if request originated from one, for updating token and setting auth header final AbstractClientBase client = (AbstractClientBase) conversation.getAttribute(CLIENT_ATTRIBUTE); // exception response if (result.isFailed()) { Throwable failure = result.getFailure(); retryOnFailure(request, conversation, retries, client, failure); return; } // response to a re-login request SalesforceHttpRequest origRequest = (SalesforceHttpRequest) conversation.getAttribute(AUTHENTICATION_REQUEST_ATTRIBUTE); if (origRequest != null) { // parse response try { session.parseLoginResponse(response, response.getContentAsString()); } catch (SalesforceException e) { // retry login request on error if we have login attempts left if (retries < maxAuthenticationRetries) { retryOnFailure(request, conversation, retries, client, e); } else { forwardFailureComplete(origRequest, null, response, e); } return; } // retry original request on success conversation.removeAttribute(AUTHENTICATION_REQUEST_ATTRIBUTE); retryRequest(origRequest, client, retries, conversation, true); return; } // response to an original request final int status = response.getStatus(); final String reason = response.getReason(); // check if login retries left if (retries >= maxAuthenticationRetries) { // forward current response forwardSuccessComplete(request, response); return; } // request failed authentication? if (status == HttpStatus.UNAUTHORIZED_401) { // REST token expiry LOG.warn("Retrying on Salesforce authentication error [{}]: [{}]", status, reason); // remember original request and send a relogin request in current conversation retryLogin(request, retries); } else if (status < HttpStatus.OK_200 || status >= HttpStatus.MULTIPLE_CHOICES_300) { // HTTP failure status // get detailed cause, if request comes from an AbstractClientBase final InputStream inputStream = getContent().length == 0 ? null : getContentAsInputStream(); final SalesforceException cause = client != null ? client.createRestException(response, inputStream) : null; if (status == HttpStatus.BAD_REQUEST_400 && cause != null && isInvalidSessionError(cause)) { // retry Bulk API call LOG.warn("Retrying on Bulk API Salesforce authentication error [{}]: [{}]", status, reason); retryLogin(request, retries); } else { // forward Salesforce HTTP failure! forwardSuccessComplete(request, response); } } } protected void retryOnFailure(SalesforceHttpRequest request, HttpConversation conversation, Integer retries, AbstractClientBase client, Throwable failure) { LOG.warn("Retrying on Salesforce authentication failure " + failure.getMessage(), failure); // retry request retryRequest(request, client, retries, conversation, true); } private boolean isInvalidSessionError(SalesforceException e) { return e.getErrors() != null && e.getErrors().size() == 1 && "InvalidSessionId".equals(e.getErrors().get(0).getErrorCode()); } private void retryLogin(SalesforceHttpRequest request, Integer retries) { final HttpConversation conversation = request.getConversation(); // remember the original request to resend conversation.setAttribute(AUTHENTICATION_REQUEST_ATTRIBUTE, request); retryRequest((SalesforceHttpRequest)session.getLoginRequest(conversation), null, retries, conversation, false); } private void retryRequest(SalesforceHttpRequest request, AbstractClientBase client, Integer retries, HttpConversation conversation, boolean copy) { // copy the request to resend // TODO handle a change in Salesforce instanceUrl, right now we retry with the same destination final Request newRequest; if (copy) { newRequest = httpClient.copyRequest(request, request.getURI()); newRequest.method(request.getMethod()); HttpFields headers = newRequest.getHeaders(); // copy cookies and host for subscriptions to avoid '403::Unknown Client' errors for (HttpField field : request.getHeaders()) { HttpHeader header = field.getHeader(); if (HttpHeader.COOKIE.equals(header) || HttpHeader.HOST.equals(header)) { headers.add(header, field.getValue()); } } } else { newRequest = request; } conversation.setAttribute(AUTHENTICATION_RETRIES_ATTRIBUTE, ++retries); Object originalRequest = conversation.getAttribute(AUTHENTICATION_REQUEST_ATTRIBUTE); LOG.debug("Retry attempt {} on authentication error for {}", retries, originalRequest != null ? originalRequest : newRequest); // update currentToken for original request if (originalRequest == null) { String currentToken = session.getAccessToken(); if (client != null) { // update client cache for this and future requests client.setAccessToken(currentToken); client.setInstanceUrl(session.getInstanceUrl()); client.setAccessToken(newRequest); } else { // plain request not made by an AbstractClientBase newRequest.header(HttpHeader.AUTHORIZATION, "OAuth " + currentToken); } } // send new async request with a new delegate conversation.updateResponseListeners(null); newRequest.onRequestBegin(getRequestAbortListener(request)); newRequest.send(null); } private Request.BeginListener getRequestAbortListener(final SalesforceHttpRequest request) { return new Request.BeginListener() { @Override public void onBegin(Request redirect) { Throwable cause = request.getAbortCause(); if (cause != null) { redirect.abort(cause); } } }; } private void forwardSuccessComplete(SalesforceHttpRequest request, Response response) { HttpConversation conversation = request.getConversation(); conversation.updateResponseListeners(null); notifier.forwardSuccessComplete(conversation.getResponseListeners(), request, response); } private void forwardFailureComplete(SalesforceHttpRequest request, Throwable requestFailure, Response response, Throwable responseFailure) { HttpConversation conversation = request.getConversation(); conversation.updateResponseListeners(null); notifier.forwardFailureComplete(conversation.getResponseListeners(), request, requestFailure, response, responseFailure); } } // no @Override annotation here to keep it compatible with Jetty 9.2, getName was added in 9.3 public String getName() { return "CamelSalesforceSecurityHandler"; } }