diff --git a/README.md b/README.md index 0f19e26..ada9d14 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ Maven NodeJS Proxy ====== [![Build Status](https://travis-ci.org/wcm-io-devops/maven-nodejs-proxy.png?branch=develop)](https://travis-ci.org/wcm-io-devops/maven-nodejs-proxy) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.wcm.devops.maven/io.wcm.devops.maven.nodejs-proxy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.wcm.devops.maven/io.wcm.devops.maven.nodejs-proxy) Maven proxy to download NodeJS binaries as Maven artifacts. diff --git a/changes.xml b/changes.xml index f79810f..491ab1c 100644 --- a/changes.xml +++ b/changes.xml @@ -23,6 +23,16 @@ xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd">
+This is a Maven Artifact Proxy for NodeJS binaries located at: " @@ -69,7 +73,7 @@ public static String build(MavenProxyConfiguration config) { + "
Every call to this repository is routed directly to this URL.
" + "Please never use this Maven repository directly in your maven builds, but only via an Repository Manager " + "which caches the resolved artifacts.
" - + "If you want to setup your own proxy get the source code:" + + "
If you want to setup your own proxy get the source code: " + "https://github.com/wcm-io-devops/maven-nodejs-proxy
" + "Examples:
" diff --git a/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResource.java b/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResource.java index b808240..6329f74 100644 --- a/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResource.java +++ b/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResource.java @@ -20,7 +20,6 @@ package io.wcm.devops.maven.nodejsproxy.resource; import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; -import io.wcm.devops.maven.nodejsproxy.MavenProxyConfiguration; import java.io.IOException; @@ -39,11 +38,14 @@ import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.annotation.Timed; +import io.wcm.devops.maven.nodejsproxy.MavenProxyConfiguration; + /** * Proxies NodeJS binaries. */ @@ -166,39 +168,8 @@ public Response getBinary( getChecksum = true; } - // return checksum from SHASUMS.txt - if (getChecksum) { - Checksums checksums = getChecksums(version); - String url = buildBinaryUrl(artifactType, version, os, arch, StringUtils.removeEnd(type, ".sha1")); - String checksum = checksums.get(url); - if (checksum != null) { - return Response.ok(checksum) - .type(MediaType.TEXT_PLAIN) - .build(); - } - else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - - // return binary file - else { - String url = buildBinaryUrl(artifactType, version, os, arch, type); - - log.info("Proxy file: {}", url); - HttpGet get = new HttpGet(url); - HttpResponse response = httpClient.execute(get); - if (response.getStatusLine().getStatusCode() == HttpServletResponse.SC_OK) { - return Response.ok(new SpoolStreamingOutput(response.getEntity())) - .type(MediaType.APPLICATION_OCTET_STREAM) - .header(CONTENT_LENGTH, response.containsHeader(CONTENT_LENGTH) ? response.getFirstHeader(CONTENT_LENGTH).getValue() : null) - .build(); - } - else { - EntityUtils.consumeQuietly(response.getEntity()); - return Response.status(Response.Status.NOT_FOUND).build(); - } - } + String url = buildBinaryUrl(artifactType, version, os, arch, StringUtils.removeEnd(type, ".sha1")); + return getBinaryWithChecksumValidation(url, version, getChecksum); } /** @@ -235,20 +206,48 @@ public Response getBinary( } String url = buildBinaryUrl(artifactType, version, null, null, StringUtils.removeEnd(type, ".sha1")); + return getBinary(url, version, getChecksum, null); + } + + private Response getBinaryWithChecksumValidation(String url, String version, boolean getChecksum) throws IOException { + // get original checksum from source directory + Checksums checksums = getChecksums(version); + if (checksums == null) { + log.info("File not found: {} - no checksum file found.", url); + return Response.status(Response.Status.NOT_FOUND).build(); + } + String checksum = checksums.get(url); + if (checksum == null) { + log.info("File not found: {} - no checksum found in checkum file.", url); + return Response.status(Response.Status.NOT_FOUND).build(); + } + return getBinary(url, version, getChecksum, checksum); + } + + private Response getBinary(String url, String version, boolean getChecksum, String expectedChecksum) throws IOException { log.info("Proxy file: {}", url); HttpGet get = new HttpGet(url); HttpResponse response = httpClient.execute(get); if (response.getStatusLine().getStatusCode() == HttpServletResponse.SC_OK) { + byte[] data = EntityUtils.toByteArray(response.getEntity()); + + // validate checksum + if (expectedChecksum != null) { + String remoteChecksum = DigestUtils.sha256Hex(data); + if (!StringUtils.equals(expectedChecksum, remoteChecksum)) { + log.warn("Reject file: {} - checksum comparison failed - expected: {}, actual: {}", url, expectedChecksum, remoteChecksum); + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + if (getChecksum) { - byte[] data = EntityUtils.toByteArray(response.getEntity()); - EntityUtils.consumeQuietly(response.getEntity()); return Response.ok(DigestUtils.sha1Hex(data)) .type(MediaType.TEXT_PLAIN) .build(); } else { - return Response.ok(new SpoolStreamingOutput(response.getEntity())) + return Response.ok(data) .type(MediaType.APPLICATION_OCTET_STREAM) .header(CONTENT_LENGTH, response.containsHeader(CONTENT_LENGTH) ? response.getFirstHeader(CONTENT_LENGTH).getValue() : null) .build(); @@ -337,11 +336,14 @@ private String buildBinaryUrl(ArtifactType artifactType, String version, String switch (artifactType) { case NODEJS: if (StringUtils.equals(os, "windows")) { - if (StringUtils.equals(arch, "x86")) { - url = config.getNodeJsBinariesUrlWindowsX86(); + if (isVersion4Up(version)) { + url = config.getNodeJsBinariesUrlWindows(); + } + else if (StringUtils.equals(arch, "x86")) { + url = config.getNodeJsBinariesUrlWindowsX86Legacy(); } else { - url = config.getNodeJsBinariesUrlWindows(); + url = config.getNodeJsBinariesUrlWindowsX64Legacy(); } } else { @@ -362,4 +364,10 @@ private String buildBinaryUrl(ArtifactType artifactType, String version, String return url; } + private boolean isVersion4Up(String version) { + DefaultArtifactVersion givenVersion = new DefaultArtifactVersion(version); + DefaultArtifactVersion minVersion = new DefaultArtifactVersion("4.0.0"); + return givenVersion.compareTo(minVersion) >= 0; + } + } diff --git a/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/SpoolStreamingOutput.java b/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/SpoolStreamingOutput.java deleted file mode 100644 index 88dc2aa..0000000 --- a/maven-nodejs-proxy/src/main/java/io/wcm/devops/maven/nodejsproxy/resource/SpoolStreamingOutput.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * #%L - * wcm.io - * %% - * Copyright (C) 2015 wcm.io - * %% - * 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. - * #L% - */ -package io.wcm.devops.maven.nodejsproxy.resource; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.StreamingOutput; - -import org.apache.commons.io.IOUtils; -import org.apache.http.HttpEntity; -import org.apache.http.util.EntityUtils; - -/** - * Spool binary data from HTTP response to JAX-RS output. - */ -class SpoolStreamingOutput implements StreamingOutput { - - private final HttpEntity httpEntity; - - public SpoolStreamingOutput(HttpEntity httpEntity) { - this.httpEntity = httpEntity; - } - - @Override - public void write(OutputStream os) throws IOException, WebApplicationException { - try (InputStream is = httpEntity.getContent()) { - IOUtils.copyLarge(is, os); - } - catch (IOException ex) { - // ignore - } - finally { - EntityUtils.consumeQuietly(httpEntity); - } - } - -} diff --git a/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResourceTest.java b/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResourceTest.java new file mode 100644 index 0000000..63ed700 --- /dev/null +++ b/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/MavenProxyResourceTest.java @@ -0,0 +1,141 @@ +/* + * #%L + * wcm.io + * %% + * Copyright (C) 2016 wcm.io + * %% + * 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. + * #L% + */ +package io.wcm.devops.maven.nodejsproxy.resource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; + +import io.dropwizard.testing.junit.ResourceTestRule; + + +public class MavenProxyResourceTest { + + // test with the following NodeJS and NPM versions + private static final String[] NODEJS_VERSIONS = { + "0.12.0", + "4.4.0" + }; + private static final String[] NODEJS_TARGETS = { + "-windows-x86.exe", + "-windows-x64.exe", + "-linux-x86.tar.gz", + "-linux-x64.tar.gz", + "-darwin-x64.tar.gz" + }; + private static final String[] NPM_VERSIONS = { + "1.4.9" + }; + private static final String[] NPM_TARGETS = { + ".tgz" + }; + + @Rule + public ResourceTestRule context = new ResourceTestRule.Builder() + .addResource(new MavenProxyResource(TestContext.getConfiguration(), TestContext.getHttpClient())) + .build(); + + @Test + public void testGetIndex() { + String path = "/"; + Response response = context.client().target(path).request().get(); + assertResponse(path, response, MediaType.TEXT_HTML); + } + + @Test + public void testGetPomNodeJS() { + for (String version : NODEJS_VERSIONS) { + String path = "/org/nodejs/dist/nodejs-binaries/" + version + "/nodejs-binaries-" + version + ".pom"; + Response response = context.client().target(path).request().get(); + assertResponse(path, response, MediaType.APPLICATION_XML); + assertTrue("Content length " + path, response.getLength() > 0); + assertSHA1(path, response); + } + } + + @Test + public void testGetPomNPM() { + for (String version : NPM_VERSIONS) { + String path = "/org/nodejs/dist/npm-binaries/" + version + "/npm-binaries-" + version + ".pom"; + Response response = context.client().target(path).request().get(); + assertResponse(path, response, MediaType.APPLICATION_XML); + assertTrue("Content length " + path, response.getLength() > 0); + assertSHA1(path, response); + } + } + + @Test + public void testGetBinaryNodeJS() { + for (String version : NODEJS_VERSIONS) { + for (String target : NODEJS_TARGETS) { + String path = "/org/nodejs/dist/nodejs-binaries/" + version + "/nodejs-binaries-" + version + target; + Response response = context.client().target(path).request().get(); + assertResponse(path, response, MediaType.APPLICATION_OCTET_STREAM); + assertSHA1(path, response); + } + } + } + + @Test + public void testGetBinaryNPM() { + for (String version : NPM_VERSIONS) { + for (String target : NPM_TARGETS) { + String path = "/org/nodejs/dist/npm-binaries/" + version + "/npm-binaries-" + version + target; + Response response = context.client().target(path).request().get(); + assertResponse(path, response, MediaType.APPLICATION_OCTET_STREAM); + assertSHA1(path, response); + } + } + } + + private void assertResponse(String path, Response response, String mediaType) { + System.out.println("Integration test: " + path); + assertEquals("HTTP status " + path, HttpStatus.SC_OK, response.getStatus()); + assertEquals("Media type " + path, mediaType, response.getMediaType().toString()); + assertTrue(response.hasEntity()); + } + + private void assertSHA1(String path, Response dataResponse) { + String sha1Path = path + ".sha1"; + Response sha1Response = context.client().target(sha1Path).request().get(); + assertResponse(sha1Path, sha1Response, MediaType.TEXT_PLAIN); + + try (InputStream is = dataResponse.readEntity(InputStream.class)) { + byte[] data = IOUtils.toByteArray(is); + String sha1 = sha1Response.readEntity(String.class); + assertEquals("SHA-1 " + path, sha1, DigestUtils.sha1Hex(data)); + } + catch (IOException ex) { + throw new RuntimeException("Error checking SHA-1 of " + path, ex); + } + } + +} diff --git a/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/TestContext.java b/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/TestContext.java new file mode 100644 index 0000000..f2311d5 --- /dev/null +++ b/maven-nodejs-proxy/src/test/java/io/wcm/devops/maven/nodejsproxy/resource/TestContext.java @@ -0,0 +1,62 @@ +/* + * #%L + * wcm.io + * %% + * Copyright (C) 2016 wcm.io + * %% + * 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. + * #L% + */ +package io.wcm.devops.maven.nodejsproxy.resource; + +import java.io.File; +import java.io.IOException; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.dropwizard.configuration.ConfigurationException; +import io.dropwizard.configuration.ConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.wcm.devops.maven.nodejsproxy.MavenProxyConfiguration; + +final class TestContext { + + private static final ObjectMapper OBJECT_MAPPER = Jackson.newObjectMapper(); + + private TestContext() { + // static methods only + } + + static MavenProxyConfiguration getConfiguration() { + ConfigurationFactory factory = new ConfigurationFactory