/**
* 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.portal.lpkg.deployer.internal;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.process.ClassPathUtil;
import com.liferay.portal.kernel.process.ProcessChannel;
import com.liferay.portal.kernel.process.ProcessConfig;
import com.liferay.portal.kernel.process.ProcessConfig.Builder;
import com.liferay.portal.kernel.process.local.LocalProcessExecutor;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.PortalClassLoaderUtil;
import com.liferay.portal.kernel.util.StreamUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Time;
import com.liferay.portal.lpkg.deployer.LPKGDeployer;
import com.liferay.portal.lpkg.deployer.LPKGVerifyException;
import com.liferay.portal.target.platform.indexer.IndexValidator;
import com.liferay.portal.target.platform.indexer.IndexValidatorFactory;
import com.liferay.portal.target.platform.indexer.Indexer;
import com.liferay.portal.target.platform.indexer.IndexerFactory;
import com.liferay.portal.util.PropsValues;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* @author Shuyang Zhou
*/
@Component(immediate = true, service = LPKGIndexValidator.class)
public class LPKGIndexValidator {
public LPKGIndexValidator() {
Builder builder = new Builder();
builder.setArguments(Arrays.asList("-Djava.awt.headless=true"));
String classpath = ClassPathUtil.buildClassPath(
IndexerFactory.class, Bundle.class,
TargetPlatformIndexerProcessCallable.class);
classpath = classpath.concat(File.pathSeparator).concat(
ClassPathUtil.getGlobalClassPath());
builder.setBootstrapClassPath(classpath);
builder.setReactClassLoader(PortalClassLoaderUtil.getClassLoader());
builder.setRuntimeClassPath(classpath);
_processConfig = builder.build();
}
@Activate
public void activate(BundleContext bundleContext) {
_enabled = GetterUtil.getBoolean(
bundleContext.getProperty("lpkg.index.validator.enabled"), true);
}
public boolean checkIntegrity(List<URI> indexURIs) {
if (Files.notExists(_integrityPropertiesFilePath)) {
if (_log.isInfoEnabled()) {
_log.info(
"Skip integrity check because " +
_integrityPropertiesFilePath + " does not exist");
}
return false;
}
Properties properties = new Properties();
try (InputStream inputStream = Files.newInputStream(
_integrityPropertiesFilePath)) {
properties.load(inputStream);
}
catch (IOException ioe) {
_log.error("Unable to read " + _integrityPropertiesFilePath, ioe);
return false;
}
Set<String> integrityKeys = new HashSet<>();
for (URI uri : indexURIs) {
integrityKeys.add(_toIntegrityKey(uri));
}
if (!integrityKeys.equals(properties.stringPropertyNames())) {
if (_log.isInfoEnabled()) {
List<String> expectedKeys = new ArrayList<>(
properties.stringPropertyNames());
Collections.sort(expectedKeys);
List<String> actualKeys = new ArrayList<>(integrityKeys);
Collections.sort(actualKeys);
_log.info(
"Running validation because expected keys: " +
expectedKeys + " do not match actual keys: " +
actualKeys);
}
return false;
}
for (URI uri : indexURIs) {
String integrityKey = _toIntegrityKey(uri);
try {
String expectedChecksum = properties.getProperty(integrityKey);
String actualChecksum = _toChecksum(uri);
if (!Objects.equals(expectedChecksum, actualChecksum)) {
if (_log.isInfoEnabled()) {
_log.info(
"Running validation because of mismatched " +
"checksum for " + integrityKey);
}
return false;
}
}
catch (Exception e) {
_log.error("Unable to generate checksum for " + uri);
return false;
}
}
if (_log.isInfoEnabled()) {
_log.info("Passed integrity check");
}
return true;
}
public void setJarFiles(List<File> jarFiles) {
_jarFiles = jarFiles;
Set<String> jarFileNames = new HashSet<>();
for (File file : jarFiles) {
jarFileNames.add(StringUtil.toLowerCase(file.getName()));
}
_jarFileNames = jarFileNames;
}
public void setLPKGDeployer(LPKGDeployer lpkgDeployer) {
_lpkgDeployer = lpkgDeployer;
}
public void updateIntegrityProperties() {
try {
List<URI> indexURIs = _getTargetPlatformIndexURIs();
Collections.sort(indexURIs);
StringBundler sb = new StringBundler(indexURIs.size() * 4);
for (URI uri : indexURIs) {
sb.append(_toIntegrityKey(uri));
sb.append(StringPool.EQUAL);
sb.append(_toChecksum(uri));
sb.append(StringPool.NEW_LINE);
}
sb.setIndex(sb.index() - 1);
Files.createDirectories(_integrityPropertiesFilePath.getParent());
Files.write(
_integrityPropertiesFilePath,
Collections.singleton(sb.toString()), StandardCharsets.UTF_8);
if (_log.isInfoEnabled()) {
_log.info("Updated " + _integrityPropertiesFilePath);
}
}
catch (Exception e) {
_log.error("Unable to update integrity properties", e);
}
}
public boolean validate(List<File> lpkgFiles) throws Exception {
if (!_enabled) {
return false;
}
long start = System.currentTimeMillis();
List<URI> allIndexURIs = new ArrayList<>();
List<URI> targetPlatformIndexURIs = _getTargetPlatformIndexURIs();
allIndexURIs.addAll(targetPlatformIndexURIs);
List<URI> lpkgIndexURIs = _indexLPKGFiles(lpkgFiles);
allIndexURIs.addAll(lpkgIndexURIs);
if (checkIntegrity(allIndexURIs)) {
return false;
}
IndexValidator indexValidator = _indexValidatorFactory.create(
targetPlatformIndexURIs);
try {
List<String> messages = indexValidator.validate(lpkgIndexURIs);
if (!messages.isEmpty()) {
StringBundler sb = new StringBundler((messages.size() * 3) + 1);
sb.append("LPKG validation failed with {");
for (String message : messages) {
sb.append("[");
sb.append(message);
sb.append("], ");
}
sb.setIndex(sb.index() - 1);
sb.append("]}");
throw new LPKGVerifyException(sb.toString());
}
}
finally {
_cleanUp(targetPlatformIndexURIs);
_cleanUp(lpkgIndexURIs);
if (_log.isInfoEnabled()) {
long duration = System.currentTimeMillis() - start;
_log.info(
String.format(
"LPKG validation time %02d:%02ds",
MILLISECONDS.toMinutes(duration),
MILLISECONDS.toSeconds(duration % Time.MINUTE)));
}
}
return true;
}
private void _cleanUp(List<URI> uris) throws MalformedURLException {
for (URI uri : uris) {
_bytesURLProtocolSupport.removeBytes(uri.toURL());
}
}
private List<URI> _getTargetPlatformIndexURIs() throws Exception {
List<File> files = new ArrayList<>();
Map<Bundle, List<Bundle>> deployedLPKGBundles =
_lpkgDeployer.getDeployedLPKGBundles();
for (Bundle bundle : deployedLPKGBundles.keySet()) {
files.add(new File(bundle.getLocation()));
}
List<URI> uris = _indexLPKGFiles(files);
byte[] bytes = null;
LocalProcessExecutor localProcessExecutor = new LocalProcessExecutor();
List<File> additionalJarFiles = new ArrayList<>(_jarFiles);
additionalJarFiles.add(
new File(PropsValues.LIFERAY_LIB_PORTAL_DIR, "util-taglib.jar"));
try {
ProcessChannel<byte[]> processChannel =
localProcessExecutor.execute(
_processConfig,
new TargetPlatformIndexerProcessCallable(
additionalJarFiles,
PropsValues.MODULE_FRAMEWORK_STOP_WAIT_TIMEOUT,
PropsValues.MODULE_FRAMEWORK_BASE_DIR + "/static",
PropsValues.MODULE_FRAMEWORK_MODULES_DIR,
PropsValues.MODULE_FRAMEWORK_PORTAL_DIR));
Future<byte[]> future = processChannel.getProcessNoticeableFuture();
bytes = future.get();
}
finally {
localProcessExecutor.destroy();
}
URL url = _bytesURLProtocolSupport.putBytes(
"liferay-target-platform", bytes);
uris.add(url.toURI());
return uris;
}
private List<URI> _indexLPKGFiles(List<File> lpkgFiles) throws Exception {
List<URI> uris = new ArrayList<>();
UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
new UnsyncByteArrayOutputStream();
try {
for (File lpkgFile : lpkgFiles) {
Indexer indexer = _indexerFactory.createLPKGIndexer(
lpkgFile, _jarFileNames);
indexer.index(unsyncByteArrayOutputStream);
String name = lpkgFile.getName();
URL url = _bytesURLProtocolSupport.putBytes(
name.substring(0, name.length() - 5),
unsyncByteArrayOutputStream.toByteArray());
unsyncByteArrayOutputStream.reset();
uris.add(url.toURI());
}
}
catch (Exception e) {
_cleanUp(uris);
throw e;
}
return uris;
}
private String _toChecksum(URI uri) throws Exception {
URL url = uri.toURL();
UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
new UnsyncByteArrayOutputStream();
StreamUtil.transfer(url.openStream(), unsyncByteArrayOutputStream);
String content = unsyncByteArrayOutputStream.toString(StringPool.UTF8);
Matcher matcher = _incrementPattern.matcher(content);
if (matcher.find()) {
String start = content.substring(0, matcher.start(1));
String end = content.substring(matcher.end(1));
content = start.concat(end);
}
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(content.getBytes(StandardCharsets.UTF_8));
return StringUtil.bytesToHexString(messageDigest.digest());
}
private String _toIntegrityKey(URI uri) {
String integrityKey = uri.getPath();
int index = integrityKey.lastIndexOf(StringPool.SLASH);
if (index != -1) {
integrityKey = integrityKey.substring(index + 1);
}
return integrityKey;
}
private static final Log _log = LogFactoryUtil.getLog(
LPKGIndexValidator.class);
private static final Pattern _incrementPattern = Pattern.compile(
"<repository( increment=\"\\d*\")");
@Reference
private BytesURLProtocolSupport _bytesURLProtocolSupport;
private boolean _enabled;
@Reference
private IndexerFactory _indexerFactory;
@Reference
private IndexValidatorFactory _indexValidatorFactory;
private final Path _integrityPropertiesFilePath = Paths.get(
PropsValues.MODULE_FRAMEWORK_BASE_DIR, Indexer.DIR_NAME_TARGET_PLATFORM,
"integrity.properties");
private Set<String> _jarFileNames;
private List<File> _jarFiles;
private LPKGDeployer _lpkgDeployer;
private final ProcessConfig _processConfig;
}