/* * Copyright 2014-2016 CyberVision, Inc. * * Licensed 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.kaaproject.kaa.server.verifiers.twitter.verifier; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.kaaproject.kaa.server.common.verifier.AbstractKaaUserVerifier; import org.kaaproject.kaa.server.common.verifier.UserVerifierCallback; import org.kaaproject.kaa.server.common.verifier.UserVerifierContext; import org.kaaproject.kaa.server.verifiers.twitter.config.gen.TwitterAvroConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class TwitterUserVerifier extends AbstractKaaUserVerifier<TwitterAvroConfig> { private static final Logger LOG = LoggerFactory.getLogger(TwitterUserVerifier.class); private static final String TWITTER_PATH = "/1.1/account/verify_credentials.json"; private static final String SIGNATURE_METHOD = "HMAC-SHA1"; private static final String ENCRYPTION_ALGO = "HmacSHA1"; private static final String REQUEST_METHOD = "GET"; private static final String REQUEST_HEADER_NAME = "Authorization"; private static final String ID = "id_str"; private static final String ERRORS = "errors"; private static final String CODE = "code"; private static final String MESSAGE = "message"; private static final String INVALID_TOKEN_CODE = "89"; private static final int HTTP_OK = 200; private static final int HTTP_BAD_REQUEST = 400; private static final int HTTP_UNATHORIZED = 401; private static final long MAX_SEC_TWITTER_REQUEST_TIME = 60; private static final HttpHost TWITTER_HOST = new HttpHost("api.twitter.com", 443, "https"); private static OAuthHeaderBuilder oAuthHeaderBuilder; private TwitterAvroConfig configuration; private ExecutorService tokenVerifiersPool; private CloseableHttpClient httpClient; @Override public void init(UserVerifierContext context, TwitterAvroConfig configuration) { LOG.info("Initializing tiwtter user verifier with context {} and configuration {}", context, configuration); this.configuration = configuration; } @Override public void checkAccessToken(String userExternalId, String tokenAndSecret, UserVerifierCallback callback) { tokenVerifiersPool.submit(new TokenVerifier(userExternalId, tokenAndSecret, callback)); } private void handleResponse(CloseableHttpResponse connection, String userExternalId, UserVerifierCallback callback, String userAccessToken) throws IOException { Map<String, Object> responseMap = getResponseMap(connection.getEntity().getContent()); String receivedUserId = String.valueOf(responseMap.get(ID)); if (!receivedUserId.equals(userExternalId)) { LOG.warn("Input token doesn't belong to the user with {} id", userExternalId); callback.onVerificationFailure("User access token " + userAccessToken + " doesn't belong to the user"); } else { LOG.trace("Input token is confirmed and belongs to the user with {} id", userExternalId); callback.onSuccess(); } } @SuppressWarnings("unchecked") private void handleBadResponse(CloseableHttpResponse connection, UserVerifierCallback callback) throws IOException { Map<String, Object> responseMap = getResponseMap(connection.getEntity().getContent()); List<Map<String, Object>> errorList = (List<Map<String, Object>>) responseMap.get(ERRORS); // there is only one error in the list Map<String, Object> error = errorList.get(0); if (INVALID_TOKEN_CODE.equals(String.valueOf(error.get(CODE)))) { LOG.trace("Access Token is invalid or expired"); callback.onTokenInvalid(); } else { LOG.trace("Unable to verify token. Error code: [{}], message[{}]", error.get(CODE), error.get(MESSAGE)); callback.onVerificationFailure("Unable to verify token. Error code: " + error.get(CODE) + ", message: " + error.get(MESSAGE)); } } private Map<String, Object> getResponseMap(InputStream inputStream) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { ObjectMapper responseMapper = new ObjectMapper(); return responseMapper.readValue(reader.readLine(), Map.class); } } protected CloseableHttpResponse establishConnection(String tokenAndSecret) throws IOException, NoSuchAlgorithmException, InvalidKeyException { HttpRequest request = null; request = new BasicHttpEntityEnclosingRequest(REQUEST_METHOD, TWITTER_PATH); String[] tokenThenSecret = tokenAndSecret.split(" "); // now user access token is tokenThenSecret[0] // and user secret is tokenAndSecret[1] request.setHeader(REQUEST_HEADER_NAME, oAuthHeaderBuilder.generateHeader(tokenThenSecret[0], tokenThenSecret[1])); return httpClient.execute(TWITTER_HOST, request); } @Override public void start() { LOG.info("twitter user verifier started"); tokenVerifiersPool = new ThreadPoolExecutor(0, configuration.getMaxParallelConnections(), MAX_SEC_TWITTER_REQUEST_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); httpClient = HttpClients.custom().setConnectionManager(connectionManager).build(); // Increase max total connection connectionManager.setMaxTotal(configuration.getMaxParallelConnections()); oAuthHeaderBuilder = new OAuthHeaderBuilder(SIGNATURE_METHOD, REQUEST_METHOD, configuration.getTwitterVerifyUrl(), ENCRYPTION_ALGO, configuration.getConsumerKey(), configuration.getConsumerSecret()); } @Override public void stop() { LOG.info("stopping twitter verifier"); tokenVerifiersPool.shutdown(); try { httpClient.close(); } catch (IOException ex) { LOG.debug("Unable to close HttpClient ", ex); } LOG.info("twitter user verifier stopped"); } @Override public Class<TwitterAvroConfig> getConfigurationClass() { return TwitterAvroConfig.class; } private class TokenVerifier implements Runnable { private final String userExternalId; private final String tokenAndSecret; private final UserVerifierCallback callback; public TokenVerifier(String userExternalId, String tokenAndSecret, UserVerifierCallback callback) { this.userExternalId = userExternalId; this.tokenAndSecret = tokenAndSecret; this.callback = callback; } @Override public void run() { LOG.trace("Started twitter token verification of [{}] tokenAndSecret", tokenAndSecret); CloseableHttpResponse closeableHttpResponse = null; try { closeableHttpResponse = establishConnection(tokenAndSecret); LOG.trace("Connection established [{}]", tokenAndSecret); int responseCode = closeableHttpResponse.getStatusLine().getStatusCode(); if (responseCode == HTTP_BAD_REQUEST || responseCode == HTTP_UNATHORIZED) { handleBadResponse(closeableHttpResponse, callback); } else if (responseCode == HTTP_OK) { handleResponse(closeableHttpResponse, userExternalId, callback, tokenAndSecret); } else { // other response codes LOG.warn("Server response code: {}, no data can be retrieved", responseCode); callback.onVerificationFailure("Server response code:" + responseCode + ", no data can be retrieved"); } } catch (IOException ex) { LOG.debug("Connection error", ex); callback.onConnectionError(ex.getMessage()); } catch (Exception ex) { LOG.debug("Unexpected error", ex); callback.onInternalError(ex.getMessage()); } finally { if (closeableHttpResponse != null) { try { closeableHttpResponse.close(); } catch (IOException ex) { LOG.debug("Connection error: can't close CloseableHttpResponse ", ex); } } } } } }