From cbee7472a4c3052f1f6dcee2d18988f05cbd0345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20D=C3=B6ring?= Date: Thu, 17 Feb 2022 00:20:05 +0100 Subject: [PATCH] #972: add trustInsecureDownloadRoot parameter to plugins While adding unit tests by default, a bug was discovered when downloading via proxy with BasicAuth. This has also been fixed. --- CHANGELOG.md | 7 + README.md | 9 +- .../frontend/mojo/InstallNodeAndNpmMojo.java | 10 + .../frontend/mojo/InstallNodeAndPnpmMojo.java | 10 + .../frontend/mojo/InstallNodeAndYarnMojo.java | 8 + frontend-plugin-core/pom.xml | 6 + .../plugins/frontend/lib/FileDownloader.java | 68 ++++--- .../plugins/frontend/lib/NPMInstaller.java | 17 +- .../plugins/frontend/lib/NodeInstaller.java | 21 +- .../plugins/frontend/lib/PNPMInstaller.java | 17 +- .../plugins/frontend/lib/YarnInstaller.java | 17 +- .../lib/DefaultFileDownloaderTest.java | 186 ++++++++++++++++++ .../src/test/resources/testkeystore.jks | Bin 0 -> 2003 bytes 13 files changed, 331 insertions(+), 45 deletions(-) create mode 100644 frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultFileDownloaderTest.java create mode 100644 frontend-plugin-core/src/test/resources/testkeystore.jks diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f002ebdf..d8e0b5fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Last public release: [![Maven Central](https://maven-badges.herokuapp.com/maven- ## Changelog +### 1.12.2 + +* add Dependency: wiremock-jre8 (2.32.0) +* add code coverage to ``DefaultFileDownloader`` +* fix bug on download files over proxy with Basic Auth +* add new configuration ``trustInsecureDownloadRoot`` to ignore insecure HTTPS downloads + ### 1.12.1 * update Dependency: Jackson (2.13.0), Mockito (4.1.0), JUnit (5.8.1), Hamcrest (2.2; now a direct dependency) diff --git a/README.md b/README.md index 4dee31f74..7cf2fc178 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,9 @@ present). http://myproxy.example.org/nodejs/ + + + false ``` @@ -120,6 +123,8 @@ You can also specify separate download roots for npm and node as they are stored server001 https://myproxy.example.org/npm/ + + false ``` @@ -166,7 +171,9 @@ https://github.com/eirslett/frontend-maven-plugin/blob/master/frontend-maven-plu http://myproxy.example.org/nodejs/ - http://myproxy.example.org/yarn/ + http://myproxy.example.org/yarn/ + + false ``` diff --git a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java index c80b3c0a5..15f38725f 100644 --- a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java +++ b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndNpmMojo.java @@ -36,6 +36,12 @@ public final class InstallNodeAndNpmMojo extends AbstractFrontendMojo { @Deprecated private String downloadRoot; + /** + * Will trust insecure download targets. This flag applies to both the {@link #nodeDownloadRoot} and {@link #npmDownloadRoot}. + */ + @Parameter(property = "trustInsecureDownloadRoot", required = false, defaultValue = "false") + private boolean trustInsecureDownloadRoot; + /** * The version of Node.js to install. IMPORTANT! Most Node.js version names start with 'v', for example 'v0.10.18' */ @@ -81,6 +87,7 @@ public void execute(FrontendPluginFactory factory) throws InstallationException factory.getNodeInstaller(proxyConfig) .setNodeVersion(nodeVersion) .setNodeDownloadRoot(nodeDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setNpmVersion(npmVersion) .setUserName(server.getUsername()) .setPassword(server.getPassword()) @@ -89,6 +96,7 @@ public void execute(FrontendPluginFactory factory) throws InstallationException .setNodeVersion(nodeVersion) .setNpmVersion(npmVersion) .setNpmDownloadRoot(npmDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setUserName(server.getUsername()) .setPassword(server.getPassword()) .install(); @@ -96,12 +104,14 @@ public void execute(FrontendPluginFactory factory) throws InstallationException factory.getNodeInstaller(proxyConfig) .setNodeVersion(nodeVersion) .setNodeDownloadRoot(nodeDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setNpmVersion(npmVersion) .install(); factory.getNPMInstaller(proxyConfig) .setNodeVersion(this.nodeVersion) .setNpmVersion(this.npmVersion) .setNpmDownloadRoot(npmDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .install(); } } diff --git a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndPnpmMojo.java b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndPnpmMojo.java index e9655a12e..43767057f 100644 --- a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndPnpmMojo.java +++ b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndPnpmMojo.java @@ -36,6 +36,12 @@ public final class InstallNodeAndPnpmMojo extends AbstractFrontendMojo { @Deprecated private String downloadRoot; + /** + * Will trust insecure download targets. This flag applies to both the {@link #nodeDownloadRoot} and {@link #pnpmDownloadRoot}. + */ + @Parameter(property = "trustInsecureDownloadRoot", required = false, defaultValue = "false") + private boolean trustInsecureDownloadRoot; + /** * The version of Node.js to install. IMPORTANT! Most Node.js version names start with 'v', for example 'v0.10.18' */ @@ -81,6 +87,7 @@ public void execute(FrontendPluginFactory factory) throws InstallationException factory.getNodeInstaller(proxyConfig) .setNodeVersion(nodeVersion) .setNodeDownloadRoot(nodeDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setNpmVersion(pnpmVersion) .setUserName(server.getUsername()) .setPassword(server.getPassword()) @@ -88,6 +95,7 @@ public void execute(FrontendPluginFactory factory) throws InstallationException factory.getPNPMInstaller(proxyConfig) .setPnpmVersion(pnpmVersion) .setPnpmDownloadRoot(npmDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setUserName(server.getUsername()) .setPassword(server.getPassword()) .install(); @@ -95,11 +103,13 @@ public void execute(FrontendPluginFactory factory) throws InstallationException factory.getNodeInstaller(proxyConfig) .setNodeVersion(nodeVersion) .setNodeDownloadRoot(nodeDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setNpmVersion(pnpmVersion) .install(); factory.getPNPMInstaller(proxyConfig) .setPnpmVersion(this.pnpmVersion) .setPnpmDownloadRoot(npmDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .install(); } } diff --git a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndYarnMojo.java b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndYarnMojo.java index e1aaf1f2b..38dc12e90 100644 --- a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndYarnMojo.java +++ b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/InstallNodeAndYarnMojo.java @@ -33,6 +33,12 @@ public final class InstallNodeAndYarnMojo extends AbstractFrontendMojo { defaultValue = YarnInstaller.DEFAULT_YARN_DOWNLOAD_ROOT) private String yarnDownloadRoot; + /** + * Will trust insecure download targets. This flag applies to both the {@link #nodeDownloadRoot} and {@link #yarnDownloadRoot}. + */ + @Parameter(property = "trustInsecureDownloadRoot", required = false, defaultValue = "false") + private boolean trustInsecureDownloadRoot; + /** * The version of Node.js to install. IMPORTANT! Most Node.js version names start with 'v', for example * 'v0.10.18' @@ -91,9 +97,11 @@ public void execute(FrontendPluginFactory factory) throws InstallationException Server server = MojoUtils.decryptServer(this.serverId, this.session, this.decrypter); if (null != server) { factory.getNodeInstaller(proxyConfig).setNodeDownloadRoot(this.nodeDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setNodeVersion(this.nodeVersion).setPassword(server.getPassword()) .setUserName(server.getUsername()).install(); factory.getYarnInstaller(proxyConfig).setYarnDownloadRoot(this.yarnDownloadRoot) + .setTrustInsecureDownloadRoot(trustInsecureDownloadRoot) .setYarnVersion(this.yarnVersion).setUserName(server.getUsername()) .setPassword(server.getPassword()).setIsYarnBerry(isYarnrcYamlFilePresent()).install(); } else { diff --git a/frontend-plugin-core/pom.xml b/frontend-plugin-core/pom.xml index 8b27067d2..cbb2d029d 100644 --- a/frontend-plugin-core/pom.xml +++ b/frontend-plugin-core/pom.xml @@ -79,6 +79,12 @@ 4.1.0 test + + com.github.tomakehurst + wiremock-jre8 + 2.32.0 + test + diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java index 9f5ff1fa1..85a60b09c 100644 --- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java +++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java @@ -8,11 +8,17 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.SSLContext; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; +import org.apache.http.auth.AuthState; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.AuthCache; import org.apache.http.client.CredentialsProvider; @@ -20,11 +26,13 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; import org.codehaus.plexus.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +49,7 @@ public DownloadException(String message){ } interface FileDownloader { - void download(String downloadUrl, String destination, String userName, String password) throws DownloadException; + void download(String downloadUrl, String destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException; } final class DefaultFileDownloader implements FileDownloader { @@ -54,7 +62,7 @@ public DefaultFileDownloader(ProxyConfig proxyConfig){ } @Override - public void download(String downloadUrl, String destination, String userName, String password) throws DownloadException { + public void download(String downloadUrl, String destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { // force tls to 1.2 since github removed weak cryptographic standards // https://blog.github.com/2018-02-02-weak-cryptographic-standards-removal-notice/ System.setProperty("https.protocols", "TLSv1.2"); @@ -66,7 +74,7 @@ public void download(String downloadUrl, String destination, String userName, St FileUtils.copyFile(new File(downloadURI), new File(destination)); } else { - CloseableHttpResponse response = execute(fixedDownloadUrl, userName, password); + CloseableHttpResponse response = execute(fixedDownloadUrl, userName, password, trustInsecureDownloadRoot); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode != 200){ throw new DownloadException("Got error code "+ statusCode +" from the server."); @@ -82,12 +90,18 @@ public void download(String downloadUrl, String destination, String userName, St } } - private CloseableHttpResponse execute(String requestUrl, String userName, String password) throws IOException { + private CloseableHttpResponse execute(String requestUrl, String userName, String password, boolean trustInsecureDownloadRoot) + throws IOException, DownloadException { CloseableHttpResponse response; + SSLContext sslContext = null; + if (trustInsecureDownloadRoot) { + LOGGER.info("Trust insecure download enabled."); + sslContext = makeSSLContextForTrustedInsecureDownloads(); + } Proxy proxy = proxyConfig.getProxyForUrl(requestUrl); if (proxy != null) { LOGGER.info("Downloading via proxy " + proxy.toString()); - return executeViaProxy(proxy, requestUrl); + return executeViaProxy(proxy, sslContext, requestUrl); } else { LOGGER.info("No proxy was configured, downloading directly"); if (StringUtils.isNotEmpty(userName) && StringUtils.isNotEmpty(password)) { @@ -100,40 +114,50 @@ private CloseableHttpResponse execute(String requestUrl, String userName, String aURL.getPort(), userName, password); - response = buildHttpClient(credentialsProvider).execute(new HttpGet(requestUrl),localContext); + response = buildHttpClient(sslContext, credentialsProvider).execute(new HttpGet(requestUrl),localContext); } else { - response = buildHttpClient(null).execute(new HttpGet(requestUrl)); + response = buildHttpClient(sslContext, null).execute(new HttpGet(requestUrl)); } } return response; } - private CloseableHttpResponse executeViaProxy(Proxy proxy, String requestUrl) throws IOException { - final CloseableHttpClient proxyClient; - if (proxy.useAuthentication()){ - proxyClient = buildHttpClient(makeCredentialsProvider(proxy.host,proxy.port,proxy.username,proxy.password)); + private CloseableHttpResponse executeViaProxy(Proxy proxy, SSLContext sslContext, String requestUrl) throws IOException { + final HttpGet request = new HttpGet(requestUrl); + request.setConfig(RequestConfig.custom() + .setProxy(new HttpHost(proxy.host, proxy.port)) + .build()); + final CloseableHttpClient proxyClient = buildHttpClient(sslContext, null); + if (proxy.useAuthentication()) { + final AuthState authState = new AuthState(); + authState.update(new BasicScheme(), new UsernamePasswordCredentials(proxy.username, proxy.password)); + final HttpClientContext httpContext = HttpClientContext.create(); + httpContext.setAttribute(HttpClientContext.PROXY_AUTH_STATE, authState); + return proxyClient.execute(request, httpContext); } else { - proxyClient = buildHttpClient(null); + return proxyClient.execute(request); } - - final HttpHost proxyHttpHost = new HttpHost(proxy.host, proxy.port); - - final RequestConfig requestConfig = RequestConfig.custom().setProxy(proxyHttpHost).build(); - - final HttpGet request = new HttpGet(requestUrl); - request.setConfig(requestConfig); - - return proxyClient.execute(request); } - private CloseableHttpClient buildHttpClient(CredentialsProvider credentialsProvider) { + private CloseableHttpClient buildHttpClient(SSLContext sslContext, CredentialsProvider credentialsProvider) { return HttpClients.custom() + .setSSLContext(sslContext) .disableContentCompression() .useSystemProperties() .setDefaultCredentialsProvider(credentialsProvider) .build(); } + private SSLContext makeSSLContextForTrustedInsecureDownloads() throws DownloadException { + try { + return SSLContextBuilder.create() + .loadTrustMaterial(TrustSelfSignedStrategy.INSTANCE) + .build(); + } catch (final NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new DownloadException("Unable to create SSLContext to trust insecure downloads.", e); + } + } + private CredentialsProvider makeCredentialsProvider(String host, int port, String username, String password) { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java index 9df7a255d..ef44116f1 100644 --- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java +++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java @@ -20,6 +20,8 @@ public class NPMInstaller { private String nodeVersion, npmVersion, npmDownloadRoot, userName, password; + private boolean trustInsecureDownloadRoot; + private final Logger logger; private final InstallConfig config; @@ -50,6 +52,11 @@ public NPMInstaller setNpmDownloadRoot(String npmDownloadRoot) { return this; } + public NPMInstaller setTrustInsecureDownloadRoot(boolean trustInsecureDownloadRoot) { + this.trustInsecureDownloadRoot = trustInsecureDownloadRoot; + return this; + } + public NPMInstaller setUserName(String userName) { this.userName = userName; return this; @@ -122,7 +129,7 @@ private void installNpm() throws InstallationException { File archive = this.config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, archive, this.userName, this.password); + downloadFileIfMissing(downloadUrl, archive, this.userName, this.password, this.trustInsecureDownloadRoot); File installDirectory = getNodeInstallDirectory(); File nodeModulesDirectory = new File(installDirectory, "node_modules"); @@ -218,16 +225,16 @@ private void extractFile(File archive, File destinationDirectory) throws Archive this.archiveExtractor.extract(archive.getPath(), destinationDirectory.getPath()); } - private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password) + private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { if (!destination.exists()) { - downloadFile(downloadUrl, destination, userName, password); + downloadFile(downloadUrl, destination, userName, password, trustInsecureDownloadRoot); } } - private void downloadFile(String downloadUrl, File destination, String userName, String password) + private void downloadFile(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { this.logger.info("Downloading {} to {}", downloadUrl, destination); - this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password); + this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password, trustInsecureDownloadRoot); } } diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java index 805c8901c..f9a707058 100644 --- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java +++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java @@ -20,6 +20,8 @@ public class NodeInstaller { private String npmVersion, nodeVersion, nodeDownloadRoot, userName, password; + private boolean trustInsecureDownloadRoot; + private final Logger logger; private final InstallConfig config; @@ -45,6 +47,11 @@ public NodeInstaller setNodeDownloadRoot(String nodeDownloadRoot) { return this; } + public NodeInstaller setTrustInsecureDownloadRoot(boolean trustInsecureDownloadRoot) { + this.trustInsecureDownloadRoot = trustInsecureDownloadRoot; + return this; + } + public NodeInstaller setNpmVersion(String npmVersion) { this.npmVersion = npmVersion; return this; @@ -137,7 +144,7 @@ private void installNodeDefault() throws InstallationException { File archive = this.config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, archive, this.userName, this.password); + downloadFileIfMissing(downloadUrl, archive, this.userName, this.password, this.trustInsecureDownloadRoot); try { extractFile(archive, tmpDirectory); @@ -225,7 +232,7 @@ private void installNodeWithNpmForWindows() throws InstallationException { File archive = this.config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, archive, this.userName, this.password); + downloadFileIfMissing(downloadUrl, archive, this.userName, this.password, this.trustInsecureDownloadRoot); extractFile(archive, tmpDirectory); @@ -281,7 +288,7 @@ private void installNodeForWindows() throws InstallationException { File binary = this.config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, binary, this.userName, this.password); + downloadFileIfMissing(downloadUrl, binary, this.userName, this.password, this.trustInsecureDownloadRoot); this.logger.info("Copying node binary from {} to {}", binary, destination); FileUtils.copyFile(binary, destination); @@ -324,16 +331,16 @@ private void extractFile(File archive, File destinationDirectory) throws Archive this.archiveExtractor.extract(archive.getPath(), destinationDirectory.getPath()); } - private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password) + private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { if (!destination.exists()) { - downloadFile(downloadUrl, destination, userName, password); + downloadFile(downloadUrl, destination, userName, password, trustInsecureDownloadRoot); } } - private void downloadFile(String downloadUrl, File destination, String userName, String password) + private void downloadFile(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { this.logger.info("Downloading {} to {}", downloadUrl, destination); - this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password); + this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password, trustInsecureDownloadRoot); } } diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PNPMInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PNPMInstaller.java index 7d27d3714..b25194ea0 100644 --- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PNPMInstaller.java +++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PNPMInstaller.java @@ -20,6 +20,8 @@ public class PNPMInstaller { private String pnpmVersion, pnpmDownloadRoot, userName, password; + private boolean trustInsecureDownloadRoot; + private final Logger logger; private final InstallConfig config; @@ -49,6 +51,11 @@ public PNPMInstaller setPnpmDownloadRoot(String pnpmDownloadRoot) { return this; } + public PNPMInstaller setTrustInsecureDownloadRoot(boolean trustInsecureDownloadRoot) { + this.trustInsecureDownloadRoot = trustInsecureDownloadRoot; + return this; + } + public PNPMInstaller setUserName(String userName) { this.userName = userName; return this; @@ -110,7 +117,7 @@ private void installPnpm() throws InstallationException { File archive = this.config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, archive, this.userName, this.password); + downloadFileIfMissing(downloadUrl, archive, this.userName, this.password, this.trustInsecureDownloadRoot); File installDirectory = getNodeInstallDirectory(); File nodeModulesDirectory = new File(installDirectory, "node_modules"); @@ -206,16 +213,16 @@ private void extractFile(File archive, File destinationDirectory) throws Archive this.archiveExtractor.extract(archive.getPath(), destinationDirectory.getPath()); } - private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password) + private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { if (!destination.exists()) { - downloadFile(downloadUrl, destination, userName, password); + downloadFile(downloadUrl, destination, userName, password, trustInsecureDownloadRoot); } } - private void downloadFile(String downloadUrl, File destination, String userName, String password) + private void downloadFile(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { this.logger.info("Downloading {} to {}", downloadUrl, destination); - this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password); + this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password, trustInsecureDownloadRoot); } } diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java index b7445d99e..6af0cb342 100644 --- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java +++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java @@ -23,6 +23,8 @@ public class YarnInstaller { private String yarnVersion, yarnDownloadRoot, userName, password; + private boolean trustInsecureDownloadRoot; + private boolean isYarnBerry; private final Logger logger; @@ -55,6 +57,11 @@ public YarnInstaller setYarnDownloadRoot(String yarnDownloadRoot) { return this; } + public YarnInstaller setTrustInsecureDownloadRoot(boolean trustInsecureDownloadRoot) { + this.trustInsecureDownloadRoot = trustInsecureDownloadRoot; + return this; + } + public YarnInstaller setUserName(String userName) { this.userName = userName; return this; @@ -121,7 +128,7 @@ private void installYarn() throws InstallationException { File archive = config.getCacheResolver().resolve(cacheDescriptor); - downloadFileIfMissing(downloadUrl, archive, userName, password); + downloadFileIfMissing(downloadUrl, archive, userName, password, trustInsecureDownloadRoot); File installDirectory = getInstallDirectory(); @@ -193,16 +200,16 @@ private void ensureCorrectYarnRootDirectory(File installDirectory, String yarnVe } } - private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password) + private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { if (!destination.exists()) { - downloadFile(downloadUrl, destination, userName, password); + downloadFile(downloadUrl, destination, userName, password, trustInsecureDownloadRoot); } } - private void downloadFile(String downloadUrl, File destination, String userName, String password) + private void downloadFile(String downloadUrl, File destination, String userName, String password, boolean trustInsecureDownloadRoot) throws DownloadException { logger.info("Downloading {} to {}", downloadUrl, destination); - fileDownloader.download(downloadUrl, destination.getPath(), userName, password); + fileDownloader.download(downloadUrl, destination.getPath(), userName, password, trustInsecureDownloadRoot); } } diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultFileDownloaderTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultFileDownloaderTest.java new file mode 100644 index 000000000..ba6f25228 --- /dev/null +++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultFileDownloaderTest.java @@ -0,0 +1,186 @@ +package com.github.eirslett.maven.plugins.frontend.lib; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +import com.github.tomakehurst.wiremock.client.MappingBuilder; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.fail; + +class DefaultFileDownloaderTest { + @RegisterExtension + static WireMockExtension downloadMock = WireMockExtension.newInstance().build(); + @RegisterExtension + static WireMockExtension secureMock = WireMockExtension.newInstance() + .options(wireMockConfig().httpsPort(44443).keystorePath("testkeystore.jks")) + .build(); + + FileDownloader fileDownloader; + String downloadUrl; + String destination; + + private static MappingBuilder defaultFileDownloadStub(final MappingBuilder mappingBuilder) { + return mappingBuilder + .willReturn(aResponse() + .withStatus(200) + .withBody("File EXE".getBytes(UTF_8))); + } + + private static MappingBuilder defaultFileDownloadStub() { + return defaultFileDownloadStub(get("/path/file.exe")); + } + + private static MappingBuilder defaultFileDownloadStubWithBasicAuth(final String userName, final String password) { + return defaultFileDownloadStub(get("/path/file.exe") + .withBasicAuth(userName, password)); + } + + private void assertDestinationFileContent() throws IOException { + assertThat(new String(Files.readAllBytes(Paths.get(destination)), UTF_8), equalTo("File EXE")); + } + + private void verifyRequestedMock(WireMockExtension downloadMock) { + downloadMock.verify(exactly(1), getRequestedFor(urlEqualTo("/path/file.exe"))); + } + + @BeforeEach + void setUp(@TempDir final Path tempDir) { + fileDownloader = new DefaultFileDownloader(new ProxyConfig(emptyList())); + downloadUrl = "http://localhost:" + downloadMock.getPort() + "/path/file.exe"; + destination = tempDir.resolve("target.exe").toAbsolutePath().toString(); + } + + @Test + void simpleFileDownload() throws DownloadException, IOException { + // setup: + downloadMock.stubFor(defaultFileDownloadStub()); + + // when: + fileDownloader.download(downloadUrl, destination, null, null, false); + + // then: + assertDestinationFileContent(); + verifyRequestedMock(downloadMock); + } + + @Test + void secureFileDownload() throws DownloadException, IOException { + // setup: + secureMock.stubFor(defaultFileDownloadStub()); + + // and: + final String downloadUrl = "https://localhost:44443/path/file.exe"; + + // expect: 'secure download will fail because of untrusted self-signed certificate' + try { + fileDownloader.download(downloadUrl, destination, null, null, false); + fail("Error on insecure HTTPS download expected."); + } catch (final DownloadException e) { + assertThat(e.getMessage(), equalTo("Could not download " + downloadUrl)); + } + + // when: 'try again by trusting self-signed certificates' + fileDownloader.download(downloadUrl, destination, null, null, true); + + // then: + assertDestinationFileContent(); + verifyRequestedMock(secureMock); + } + + @Test + void fileDownloadWithBasicAuth() throws DownloadException, IOException { + // setup: + downloadMock.stubFor(defaultFileDownloadStubWithBasicAuth("USERNAME", "PASSWORD")); + + // when: + fileDownloader.download(downloadUrl, destination, "USERNAME", "PASSWORD", false); + + // then: + assertDestinationFileContent(); + verifyRequestedMock(downloadMock); + } + + @Test + void fileDownloadOverProxy() throws DownloadException, IOException { + // setup: + downloadMock.stubFor(defaultFileDownloadStub()); + + // and: + final String downloadUrl = "http://example.com/path/file.exe"; + + // and: + final ProxyConfig.Proxy proxy = new ProxyConfig.Proxy("Proxy ID", "http", "localhost", downloadMock.getPort(), null, null, "google.com"); + final FileDownloader fileDownloader = new DefaultFileDownloader(new ProxyConfig(singletonList(proxy))); + + // when: + fileDownloader.download(downloadUrl, destination, null, null, false); + + // then: + assertDestinationFileContent(); + verifyRequestedMock(downloadMock); + } + + @Test + void fileDownloadOverProxyWithAuthorization() throws DownloadException, IOException { + // setup: + downloadMock.stubFor(defaultFileDownloadStubWithBasicAuth("USERNAME", "PASSWORD")); + + // and: + final String downloadUrl = "http://example.com/path/file.exe"; + + // and: + final ProxyConfig.Proxy proxy = new ProxyConfig.Proxy("Proxy ID", "http", "localhost", downloadMock.getPort(), "USERNAME", "PASSWORD", "google.com"); + final FileDownloader fileDownloader = new DefaultFileDownloader(new ProxyConfig(singletonList(proxy))); + + // when: + fileDownloader.download(downloadUrl, destination, null, null, true); + + // then: + assertDestinationFileContent(); + verifyRequestedMock(downloadMock); + } + + @Test + void downloadFromFileUri(@TempDir final Path tempDir) throws DownloadException, IOException { + // setup: + final Path localFileToDownload = tempDir.resolve("file.exe"); + Files.write(localFileToDownload, "File EXE".getBytes(UTF_8)); + final String downloadUrl = localFileToDownload.toUri().toString(); + + // when: + fileDownloader.download(downloadUrl, destination, null, null, false); + + // then: + assertDestinationFileContent(); + } + + @Test + void unavailableDownloadFails() { + // expect: + try { + fileDownloader.download("http://localhost:" + downloadMock.getPort() + "/path/file.exe", destination, null, null, false); + } catch (final DownloadException e) { + assertThat(e.getMessage(), equalTo("Got error code 404 from the server.")); + verifyRequestedMock(downloadMock); + } + } +} diff --git a/frontend-plugin-core/src/test/resources/testkeystore.jks b/frontend-plugin-core/src/test/resources/testkeystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..0abbbd7b4c13735e2816813e0c25cad0168e08dc GIT binary patch literal 2003 zcmchXX*kpi8^&k;^EXD0ImW)s;E*jb8>6vaNYPLU*=1)gdzL|DIYO2}**f+;5{k&! zY6#&-LPxTUC8Q}7kweEh@ABd8`}^VkaQ~h!&vjqVb04w~*$@Z>I1=<9arlONyN4gS zXs}k&<7iTXivtR9Ncb@&I244Q08mgU2Ly}*lFTKGVjS8k147pPD2@dEn#eZ0XhvY6 zOWoweH$~0K1G<3D^)tHoaQ56Qgz8zZz&bAzt!5j{9UD&r#tG)s!I%qLv#?FcnazRe)04Ok;n^!#}c>|B3&tMs&*nj7@EXw9AYvbtT(oei_JC z>JEA#{gQM6<#2sHR;VviCCliIRMQYE?A0D){{G-dMP1EtR5j-`casZ^R3Q-_@hcWhv1I3 zyv;=o56}G3N5-)qzi=xz84EIhs%+RR%xM{H$`r;z=;2icL;n0p)fTAdnW?;Hq<$Hd zho2r-zU7Wi9WdpI*{GJxb;L06gY@@R!sdvEF8uwCPqB^!fq{`TBZZ|AP znP+kOxa(C_eWdf_2#mnN0Tb$NI1oFuY|U9?Gb%pTm_`-Nxw(ku*HV>;EJ&9at45Od z9hoUwQ%s_%Mb9kkXK^n?=0Yz)pWK7h@tRdp&MYk3X}+Kr0X zr>NB8Z-ue3E5x+cTdla)ZHvY>STC-;tAb>~_uAemXUzMEhUqbygbL>T*3}GIzw^4u znut@Sy%(2QsbY%dQFD%^2D228?_h?`#0w!Ami2gIA7U-~7xW*PL75@HQ=IO*(u?|C zMkXAgWU>=_(i6Zy7&|#tH^RQo&8~d&d`|1X@rm$|$XJef(lJ4GILZhjRAp~1Wppv% zdJR0ijBM>#R;@Vt!fg%IQw2SPPR!b}=VOX_9w!#&52j|GC7APS8)b59&KR*QJNw=`jP*?gt}Kq>J^ z0&j4+gyV0!NDT>lljFiQqDHg0s>ryOtUsCG=+dzo*NqBk|7I#$re+S`o-ILg& zW;UN;q^~!r_gq7tk9qIXt#W&ysqAda&6u%BhU+N}ez%e7%7*2}{Nh7Wj70E_>wA2^ zq^<5m1<~_Ld|$NG2|VrLWd$M&tWS1yxNKCnJr~rnO=nrJ=MGMYnq`OczialvH`Z!Y z#jrQSp>*OaHr4WoBgi6h|tTj&L|zrwTDevvV5LY=1YVU z#G8eER$F*3b0A8yn2=G~PZf^em`JV-?(Z*4#Jr?6wT_xN%-qp}ivZ>7I5nKWQG;Mm zAP;0DcG=u8$;Q_L!{6gGFzHM`5cvy+aZLpP literal 0 HcmV?d00001