/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.sync.engine.lan.session;
import com.liferay.sync.engine.document.library.event.LanDownloadFileEvent;
import com.liferay.sync.engine.document.library.handler.LanDownloadFileHandler;
import com.liferay.sync.engine.lan.util.LanPEMParserUtil;
import com.liferay.sync.engine.lan.util.LanTokenUtil;
import com.liferay.sync.engine.model.ModelListener;
import com.liferay.sync.engine.model.SyncAccount;
import com.liferay.sync.engine.model.SyncFile;
import com.liferay.sync.engine.model.SyncLanClient;
import com.liferay.sync.engine.service.SyncAccountService;
import com.liferay.sync.engine.service.SyncLanClientService;
import com.liferay.sync.engine.service.SyncLanEndpointService;
import com.liferay.sync.engine.util.GetterUtil;
import com.liferay.sync.engine.util.PropsValues;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Dennis Ju
*/
public class LanSession {
public static ExecutorService getExecutorService() {
if (_queryExecutorService != null) {
return _queryExecutorService;
}
_queryExecutorService = new ThreadPoolExecutor(
PropsValues.SYNC_LAN_SESSION_QUERY_POOL_MAX_SIZE,
PropsValues.SYNC_LAN_SESSION_QUERY_POOL_MAX_SIZE, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
_queryExecutorService.allowCoreThreadTimeOut(true);
return _queryExecutorService;
}
public static LanSession getLanSession() {
if (_lanSession == null) {
_lanSession = new LanSession();
}
return _lanSession;
}
public static ModelListener<SyncAccount> getSyncAccountListener() {
if (_syncAccountListener != null) {
return _syncAccountListener;
}
_syncAccountListener = new ModelListener<SyncAccount>() {
@Override
public void onCreate(SyncAccount syncAccount) {
createLanSession(syncAccount);
}
@Override
public void onRemove(SyncAccount syncAccount) {
createLanSession(syncAccount);
}
@Override
public void onUpdate(
SyncAccount syncAccount, Map<String, Object> originalValues) {
if (originalValues.containsKey("active") ||
originalValues.containsKey("lanCertificate") ||
originalValues.containsKey("lanKey")) {
createLanSession(syncAccount);
}
}
protected void createLanSession(SyncAccount syncAccount) {
if (syncAccount.isLanEnabled()) {
_lanSession = new LanSession();
}
}
};
return _syncAccountListener;
}
public LanSession() {
_downloadHttpClient = _createHttpClient(
PropsValues.SYNC_LAN_SESSION_DOWNLOAD_CONNECT_TIMEOUT,
PropsValues.SYNC_LAN_SESSION_DOWNLOAD_MAX_PER_ROUTE,
PropsValues.SYNC_LAN_SESSION_DOWNLOAD_MAX_TOTAL,
PropsValues.SYNC_LAN_SESSION_DOWNLOAD_SOCKET_TIMEOUT);
_queryHttpClient = _createHttpClient(
PropsValues.SYNC_LAN_SESSION_QUERY_CONNECT_TIMEOUT,
PropsValues.SYNC_LAN_SESSION_QUERY_POOL_MAX_SIZE,
PropsValues.SYNC_LAN_SESSION_QUERY_POOL_MAX_SIZE,
PropsValues.SYNC_LAN_SESSION_QUERY_SOCKET_TIMEOUT);
}
public HttpGet downloadFile(LanDownloadFileEvent lanDownloadFileEvent)
throws Exception {
final SyncFile syncFile =
(SyncFile)lanDownloadFileEvent.getParameterValue("syncFile");
SyncLanClientQueryResult syncLanClientQueryResult = findSyncLanClient(
syncFile);
final LanDownloadFileHandler lanDownloadFileHandler =
(LanDownloadFileHandler)lanDownloadFileEvent.getHandler();
if (syncLanClientQueryResult == null) {
lanDownloadFileHandler.handleException(
new NoSuchSyncLanClientException());
return null;
}
if (syncLanClientQueryResult.getConnectionsCount() >=
syncLanClientQueryResult.getMaxConnections()) {
lanDownloadFileHandler.queueDownload();
return null;
}
final String url = _getUrl(
syncLanClientQueryResult.getSyncLanClient(), syncFile);
final HttpGet httpGet = new HttpGet(url);
httpGet.addHeader(
"lanToken",
LanTokenUtil.decryptLanToken(
syncFile.getLanTokenKey(),
syncLanClientQueryResult.getEncryptedToken()));
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
if (_logger.isTraceEnabled()) {
_logger.trace(
"Downloading {} from {}",
syncFile.getFilePathName(), url);
}
_downloadHttpClient.execute(
httpGet, lanDownloadFileHandler,
HttpClientContext.create());
}
catch (Exception e) {
lanDownloadFileHandler.handleException(e);
}
}
};
_downloadExecutorService.execute(runnable);
return httpGet;
}
protected Callable<SyncLanClientQueryResult>
createSyncLanClientQueryResultCallable(
final SyncLanClient syncLanClient, SyncFile syncFile) {
String url = _getUrl(syncLanClient, syncFile);
final HttpHead httpHead = new HttpHead(url);
return new Callable<SyncLanClientQueryResult>() {
@Override
public SyncLanClientQueryResult call() throws Exception {
SyncLanClientQueryResult syncLanClientQueryResult =
new SyncLanClientQueryResult();
syncLanClientQueryResult.setSyncLanClient(syncLanClient);
HttpResponse httpResponse = _queryHttpClient.execute(
httpHead, HttpClientContext.create());
Header connectionsCountHeader = httpResponse.getFirstHeader(
"connectionsCount");
if (connectionsCountHeader == null) {
return null;
}
syncLanClientQueryResult.setConnectionsCount(
GetterUtil.getInteger(connectionsCountHeader.getValue()));
Header downloadRateHeader = httpResponse.getFirstHeader(
"downloadRate");
if (downloadRateHeader == null) {
return null;
}
syncLanClientQueryResult.setDownloadRate(
GetterUtil.getInteger(downloadRateHeader.getValue()));
Header encryptedTokenHeader = httpResponse.getFirstHeader(
"encryptedToken");
if (encryptedTokenHeader == null) {
return null;
}
syncLanClientQueryResult.setEncryptedToken(
encryptedTokenHeader.getValue());
Header maxConnectionsHeader = httpResponse.getFirstHeader(
"maxConnections");
if (maxConnectionsHeader == null) {
return null;
}
syncLanClientQueryResult.setMaxConnections(
GetterUtil.getInteger(maxConnectionsHeader.getValue()));
return syncLanClientQueryResult;
}
};
}
protected SyncLanClientQueryResult findSyncLanClient(SyncFile syncFile)
throws Exception {
SyncAccount syncAccount = SyncAccountService.fetchSyncAccount(
syncFile.getSyncAccountId());
List<String> syncLanClientUuids =
SyncLanEndpointService.findSyncLanClientUuids(
syncAccount.getLanServerUuid(), syncFile.getRepositoryId());
if (syncLanClientUuids.isEmpty()) {
return null;
}
final List<Callable<SyncLanClientQueryResult>>
syncLanClientQueryResultCallables = Collections.synchronizedList(
new ArrayList<Callable<SyncLanClientQueryResult>>(
syncLanClientUuids.size()));
for (String syncLanClientUuid : syncLanClientUuids) {
SyncLanClient syncLanClient =
SyncLanClientService.fetchSyncLanClient(syncLanClientUuid);
syncLanClientQueryResultCallables.add(
createSyncLanClientQueryResultCallable(
syncLanClient, syncFile));
}
int queryPoolSize = Math.min(
syncLanClientUuids.size(),
PropsValues.SYNC_LAN_SESSION_QUERY_POOL_MAX_SIZE);
List<Future<SyncLanClientQueryResult>>
pendingSyncLanClientQueryResults = new ArrayList<>(queryPoolSize);
ExecutorCompletionService<SyncLanClientQueryResult>
executorCompletionService = new ExecutorCompletionService<>(
getExecutorService());
for (int i = 0; i < queryPoolSize; i++) {
Callable<SyncLanClientQueryResult> callable =
new Callable<SyncLanClientQueryResult>() {
@Override
public synchronized SyncLanClientQueryResult call()
throws Exception {
if (syncLanClientQueryResultCallables.isEmpty()) {
return null;
}
Callable<SyncLanClientQueryResult>
syncLanClientQueryResultCallable =
syncLanClientQueryResultCallables.remove(0);
try {
return syncLanClientQueryResultCallable.call();
}
catch (Exception e) {
return call();
}
}
};
pendingSyncLanClientQueryResults.add(
executorCompletionService.submit(callable));
}
List<Future<SyncLanClientQueryResult>>
completedSyncLanClientQueryResult = new ArrayList<>(queryPoolSize);
long timeout = PropsValues.SYNC_LAN_SESSION_QUERY_TOTAL_TIMEOUT;
long endTime = System.currentTimeMillis() + timeout;
for (int i = 0; i < queryPoolSize; i++) {
Future<SyncLanClientQueryResult> future =
executorCompletionService.poll(timeout, TimeUnit.MILLISECONDS);
if (future == null) {
for (Future<SyncLanClientQueryResult>
pendingSyncLanClientQueryResult :
pendingSyncLanClientQueryResults) {
if (!pendingSyncLanClientQueryResult.isDone()) {
pendingSyncLanClientQueryResult.cancel(true);
}
}
break;
}
completedSyncLanClientQueryResult.add(future);
timeout = endTime - System.currentTimeMillis();
}
SyncLanClientQueryResult candidateSyncLanClientQueryResult = null;
int candidateDownloadRatePerConnection = 0;
for (Future<SyncLanClientQueryResult> completedFuture :
completedSyncLanClientQueryResult) {
SyncLanClientQueryResult syncLanClientQueryResult = null;
try {
syncLanClientQueryResult = completedFuture.get();
}
catch (Exception e) {
continue;
}
if (syncLanClientQueryResult == null) {
continue;
}
if (syncLanClientQueryResult.getConnectionsCount() >=
syncLanClientQueryResult.getMaxConnections()) {
if (candidateSyncLanClientQueryResult == null) {
candidateSyncLanClientQueryResult =
syncLanClientQueryResult;
}
continue;
}
if (syncLanClientQueryResult.getConnectionsCount() == 0) {
return syncLanClientQueryResult;
}
int downloadRatePerConnection =
syncLanClientQueryResult.getDownloadRate() /
(syncLanClientQueryResult.getConnectionsCount() + 1);
if (downloadRatePerConnection >=
candidateDownloadRatePerConnection) {
candidateDownloadRatePerConnection = downloadRatePerConnection;
candidateSyncLanClientQueryResult = syncLanClientQueryResult;
}
}
return candidateSyncLanClientQueryResult;
}
private static HttpClient _createHttpClient(
int connectTimeout, int maxPerRoute, int maxTotal, int socketTimeout) {
RegistryBuilder<ConnectionSocketFactory> registryBuilder =
RegistryBuilder.create();
try {
registryBuilder.register("https", _getSSLSocketFactory());
}
catch (Exception e) {
_logger.error(e.getMessage(), e);
}
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(registryBuilder.build());
connectionManager.setDefaultMaxPerRoute(maxPerRoute);
connectionManager.setMaxTotal(maxTotal);
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
RequestConfig.Builder builder = RequestConfig.custom();
builder.setConnectTimeout(connectTimeout);
builder.setSocketTimeout(socketTimeout);
httpClientBuilder.setDefaultRequestConfig(builder.build());
httpClientBuilder.setConnectionManager(connectionManager);
return httpClientBuilder.build();
}
private static SSLConnectionSocketFactory _getSSLSocketFactory()
throws Exception {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
for (SyncAccount syncAccount : SyncAccountService.findAll()) {
if (!syncAccount.isActive() || !syncAccount.isLanEnabled()) {
continue;
}
try {
PrivateKey privateKey = LanPEMParserUtil.parsePrivateKey(
syncAccount.getLanKey());
if (privateKey == null) {
_logger.error(
"SyncAccount {} missing valid private key",
syncAccount.getSyncAccountId());
continue;
}
X509Certificate x509Certificate =
LanPEMParserUtil.parseX509Certificate(
syncAccount.getLanCertificate());
if (x509Certificate == null) {
_logger.error(
"SyncAccount {} missing valid certificate",
syncAccount.getSyncAccountId());
continue;
}
keyStore.setCertificateEntry(
syncAccount.getLanServerUuid(), x509Certificate);
keyStore.setKeyEntry(
syncAccount.getLanServerUuid(), privateKey,
"".toCharArray(), new Certificate[] {x509Certificate});
}
catch (Exception e) {
_logger.error(e.getMessage(), e);
}
}
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "".toCharArray());
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
keyManagerFactory.getKeyManagers(),
trustManagerFactory.getTrustManagers(), null);
return new SNISSLConnectionSocketFactory(
sslContext, new NoopHostnameVerifier());
}
private static String _getUrl(
SyncLanClient syncLanClient, SyncFile syncFile) {
SyncAccount syncAccount = SyncAccountService.fetchSyncAccount(
syncFile.getSyncAccountId());
StringBuilder sb = new StringBuilder();
sb.append("https://");
sb.append(syncLanClient.getHostname());
sb.append(":");
sb.append(syncLanClient.getPort());
sb.append("/");
sb.append(syncAccount.getLanServerUuid());
sb.append("/");
sb.append(syncFile.getRepositoryId());
sb.append("/");
sb.append(syncFile.getTypePK());
sb.append("/");
sb.append(syncFile.getVersionId());
return sb.toString();
}
private static final Logger _logger = LoggerFactory.getLogger(
LanSession.class);
private static LanSession _lanSession;
private static ThreadPoolExecutor _queryExecutorService;
private static ModelListener<SyncAccount> _syncAccountListener;
private final ExecutorService _downloadExecutorService =
Executors.newCachedThreadPool();
private final HttpClient _downloadHttpClient;
private final HttpClient _queryHttpClient;
}