diff --git a/.locker/pom.xml b/.locker/pom.xml index b3afb60..b4a45fe 100644 --- a/.locker/pom.xml +++ b/.locker/pom.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 io.mvnpm - 3.0.28-SNAPSHOT + 3.0.29-SNAPSHOT mvnpm-locker pom @@ -142,7 +142,7 @@ org.mvnpm marked - 12.0.0 + 12.0.1 provided diff --git a/pom.xml b/pom.xml index 802d14d..50eebe7 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.mvnpm mvnpm - 3.0.28-SNAPSHOT + 3.0.29-SNAPSHOT mvnpm Maven on NPM https://mvnpm.org/ @@ -53,15 +53,27 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.8.1 + 3.8.2 true 3.2.5 2.23.0 1.9.0 ${quarkus.platform.version} + + 1.5.5 1.3.1 1.0.10 + 24.3.7 + 1.7.5 + 1.0.6 + 6.1.0 + 12.0.1 + 1.0.0 + 1.0.0 + + 0.0.1 + 1.3.0 @@ -147,6 +159,17 @@ io.quarkus quarkus-jdbc-postgresql + + + org.pgpainless + pgpainless-core + ${pgpainless.version} + + + org.pgpainless + pgpainless-sop + ${pgpainless.version} + io.mvnpm @@ -168,49 +191,49 @@ org.mvnpm.at.mvnpm vaadin-webcomponents - 24.3.7 + ${vaadin.version} provided org.mvnpm.at.vaadin router - 1.7.5 + ${vaadin-router.version} provided - + org.mvnpm.at.mvnpm codeblock - 1.0.6 + ${codeblock.version} provided org.mvnpm compare-versions - 6.1.0 + ${compare-versions.version} provided org.mvnpm marked - 12.0.0 + ${marked.version} provided org.mvnpm.at.quarkus-webcomponents card - 1.0.0 + ${card.version} provided org.mvnpm.at.quarkus-webcomponents badge - 1.0.0 + ${badge.version} provided @@ -227,13 +250,13 @@ io.quarkiverse.playwright quarkus-playwright - 0.0.1 + ${playwright.version} test io.mvnpm esbuild-java - 1.2.0 + ${esbuild-java.version} test diff --git a/src/main/java/io/mvnpm/composite/CompositeCreator.java b/src/main/java/io/mvnpm/composite/CompositeCreator.java index a8ec819..538d1d1 100644 --- a/src/main/java/io/mvnpm/composite/CompositeCreator.java +++ b/src/main/java/io/mvnpm/composite/CompositeCreator.java @@ -46,6 +46,7 @@ import io.mvnpm.file.FileStore; import io.mvnpm.file.FileType; import io.mvnpm.file.FileUtil; +import io.mvnpm.file.KeyHolder; import io.mvnpm.importmap.Aggregator; import io.mvnpm.importmap.ImportsDataBinding; import io.mvnpm.maven.MavenRepositoryService; @@ -75,6 +76,9 @@ public class CompositeCreator { @Inject NpmRegistryFacade npmRegistryFacade; + @Inject + KeyHolder keyHolder; + private final MavenXpp3Reader mavenXpp3Writer = new MavenXpp3Reader(); public void buildAllComposites() { @@ -353,7 +357,7 @@ private void mergeSource(Model pom, List dependencies) throws IOExce } // Also create the Sha1 FileUtil.createSha1(outputJar); - FileUtil.createAsc(outputJar); + FileUtil.createAsc(keyHolder.getSecretKeyRing(), outputJar); FileUtil.createMd5(outputJar); } } diff --git a/src/main/java/io/mvnpm/file/FileUtil.java b/src/main/java/io/mvnpm/file/FileUtil.java index 198962e..c186232 100644 --- a/src/main/java/io/mvnpm/file/FileUtil.java +++ b/src/main/java/io/mvnpm/file/FileUtil.java @@ -17,8 +17,15 @@ import jakarta.ws.rs.core.StreamingOutput; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.sop.SOPImpl; + import io.mvnpm.Constants; import io.quarkus.logging.Log; +import sop.ByteArrayAndResult; +import sop.ReadyWithResult; +import sop.SOP; +import sop.SigningResult; public class FileUtil { @@ -151,34 +158,34 @@ private static String getMd5(java.io.InputStream inputStream) { } } - public static boolean createAsc(Path localFilePath) { - return FileUtil.createAsc(localFilePath, false); - } + public static boolean createAsc(PGPSecretKeyRing secretKeyRing, Path localFilePath) { + if (secretKeyRing != null) { + String outputFile = localFilePath.toString() + Constants.DOT_ASC; + Path ascFileOutput = Paths.get(outputFile); + synchronized (ascFileOutput) { + if (!Files.exists(ascFileOutput)) { + + try { + byte[] jarFileBytes = Files.readAllBytes(localFilePath); + + SOP sop = new SOPImpl(); + ReadyWithResult readyWithResult = sop.detachedSign() + .key(secretKeyRing.getSecretKey().getEncoded()) + .data(jarFileBytes); - public static boolean createAsc(Path localFilePath, boolean force) { - String outputFile = localFilePath.toString() + Constants.DOT_ASC; - Path f = Paths.get(outputFile); - synchronized (f) { - if (!Files.exists(f) || force) { - try { - Process process = Runtime.getRuntime().exec(GPG_COMMAND + localFilePath.toString()); - // Set a timeout of 10 seconds - long timeout = 20; - boolean processFinished = process.waitFor(timeout, TimeUnit.SECONDS); - - if (!processFinished) { - process.destroy(); // If the process doesn't finish, we can destroy it + ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); + + byte[] detachedSignature = bytesAndResult.getBytes(); + + Files.write(ascFileOutput, detachedSignature); + } catch (IOException e) { + e.printStackTrace(); return false; - } else { - // Process finished within the timeout - int exitCode = process.exitValue(); - process.destroy(); - return true; } - } catch (InterruptedException | IOException ex) { - throw new IllegalStateException(ex); } } + } else { + return false; } return true; } @@ -194,6 +201,4 @@ private static byte[] getMessageDigest(InputStream inputStream, String algorithm return md.digest(); } - - private static final String GPG_COMMAND = "gpg -ab "; } diff --git a/src/main/java/io/mvnpm/file/KeyHolder.java b/src/main/java/io/mvnpm/file/KeyHolder.java new file mode 100644 index 0000000..347505c --- /dev/null +++ b/src/main/java/io/mvnpm/file/KeyHolder.java @@ -0,0 +1,51 @@ +package io.mvnpm.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.pgpainless.PGPainless; + +import io.quarkus.runtime.StartupEvent; + +/** + * Holds the key for signing the files + * + * @author Phillip Kruger (phillip.kruger@gmail.com) + */ +@ApplicationScoped +public class KeyHolder { + + private PGPSecretKeyRing secretKeyRing = null; + + @ConfigProperty(name = "mvnpm.asckey.path") + Optional asckeyPath; + + void onStart(@Observes StartupEvent ev) { + if (asckeyPath.isPresent()) { + try { + Path keyFilePath = Paths.get(asckeyPath.get()); + byte[] keyBytes = Files.readAllBytes(keyFilePath); + this.secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyBytes); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } + + public PGPSecretKeyRing getSecretKeyRing() { + return this.secretKeyRing; + } + + public boolean hasSecretKeyRing() { + return this.secretKeyRing != null; + } + +} diff --git a/src/main/java/io/mvnpm/file/type/JarClient.java b/src/main/java/io/mvnpm/file/type/JarClient.java index cd5bd6d..b6a44cf 100644 --- a/src/main/java/io/mvnpm/file/type/JarClient.java +++ b/src/main/java/io/mvnpm/file/type/JarClient.java @@ -27,7 +27,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; -import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.IOUtils; import io.mvnpm.Constants; import io.mvnpm.file.FileStore; @@ -114,10 +114,10 @@ private void tgzToJar(io.mvnpm.npm.model.Package p, Path tgzPath, JarArchiveOutp try (InputStream tgzInputStream = Files.newInputStream(tgzPath); GzipCompressorInputStream gzipInputStream = new GzipCompressorInputStream(tgzInputStream); - TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(gzipInputStream);) { + TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(gzipInputStream)) { final Map toTgz = new LinkedHashMap<>(); - for (TarArchiveEntry entry = tarArchiveInputStream.getNextTarEntry(); entry != null; entry = tarArchiveInputStream - .getNextTarEntry()) { + for (TarArchiveEntry entry = tarArchiveInputStream.getNextEntry(); entry != null; entry = tarArchiveInputStream + .getNextEntry()) { tgzEntryToJarEntry(p, entry, tarArchiveInputStream, toTgz, jarOutput); } if (!toTgz.isEmpty()) { @@ -135,33 +135,37 @@ private void tgzEntryToJarEntry(io.mvnpm.npm.model.Package p, ArchiveEntry entry String name = entry.getName(); final boolean shouldAdd = !matches(FILES_TO_EXCLUDE, name); final boolean shouldTgz = matches(FILES_TO_TGZ, name); - if (shouldAdd || shouldTgz) { - name = name.replaceFirst(NPM_ROOT, Constants.EMPTY); - // do not add entries that will result in invalid zip file systems that will not be able to be opened - // by quarkus because it uses the ZipFileSystem implementation. - final String jarEntryPath = MVN_ROOT + importMapRoot + name; - final String tarEntryPath = importMapRoot + name; - // paths that include "/./" or "/../" as path element are invalid - if (jarEntryPath.startsWith("./") || jarEntryPath.contains("/./") - || (shouldTgz && (tarEntryPath.startsWith(".") || tarEntryPath.contains("/./")))) { - return; - } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BufferedOutputStream bos = new BufferedOutputStream(baos, bufferSize)) { - IOUtils.copy(tar, bos, bufferSize); - bos.flush(); - baos.flush(); - if (shouldAdd) { - writeJarEntry(jarOutput, jarEntryPath, baos.toByteArray()); - } else { - // We don't add the META-INF because the tgz is already in META-INF - toTgz.put("resources" + importMapRoot + name, baos.toByteArray()); - } + name = name.replaceFirst(NPM_ROOT, Constants.EMPTY); + // do not add entries that will result in invalid zip file systems that will not be able to be opened + // by quarkus because it uses the ZipFileSystem implementation. + final String jarEntryPath = MVN_ROOT + importMapRoot + name; + final String tarEntryPath = importMapRoot + name; + final boolean isRelativeLink = isRelativeLink(jarEntryPath, tarEntryPath, shouldTgz); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BufferedOutputStream bos = new BufferedOutputStream(baos, bufferSize)) { + IOUtils.copy(tar, bos, bufferSize); + bos.flush(); + baos.flush(); + if (shouldAdd && !isRelativeLink) { + writeJarEntry(jarOutput, jarEntryPath, baos.toByteArray()); + } else if (shouldTgz && !isRelativeLink) { + // We don't add the META-INF because the tgz is already in META-INF + toTgz.put("resources" + importMapRoot + name, baos.toByteArray()); } } } + private boolean isRelativeLink(final String jarEntryPath, final String tarEntryPath, final boolean shouldTgz) { + // paths that include "/./" or "/../" as path element are invalid + if (jarEntryPath.startsWith("./") || jarEntryPath.contains("/./") + || (shouldTgz && (tarEntryPath.startsWith(".") || tarEntryPath.contains("/./")))) { + return true; + } + return false; + } + private byte[] tarGz(Map toCompress) throws IOException { // Step 1, 2 and 3: Create tar archive from map ByteArrayOutputStream tarOutput = new ByteArrayOutputStream(); diff --git a/src/main/java/io/mvnpm/newfile/AscService.java b/src/main/java/io/mvnpm/newfile/AscService.java index 1556c8b..88e1ac8 100644 --- a/src/main/java/io/mvnpm/newfile/AscService.java +++ b/src/main/java/io/mvnpm/newfile/AscService.java @@ -1,9 +1,11 @@ package io.mvnpm.newfile; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import io.mvnpm.file.FileStoreEvent; import io.mvnpm.file.FileUtil; +import io.mvnpm.file.KeyHolder; import io.quarkus.logging.Log; import io.quarkus.vertx.ConsumeEvent; import io.smallrye.common.annotation.Blocking; @@ -16,10 +18,13 @@ @ApplicationScoped public class AscService { + @Inject + KeyHolder keyHolder; + @ConsumeEvent("new-file-created") @Blocking public void newFileCreated(FileStoreEvent fse) { - boolean success = FileUtil.createAsc(fse.filePath()); + boolean success = FileUtil.createAsc(keyHolder.getSecretKeyRing(), fse.filePath()); if (!success) { Log.warn("file signed " + fse.filePath() + ".asc [failed] trying again"); } diff --git a/src/test/java/io/mvnpm/MavenRepositoryApiTest.java b/src/test/java/io/mvnpm/MavenRepositoryApiTest.java index cf69a22..0e351a5 100644 --- a/src/test/java/io/mvnpm/MavenRepositoryApiTest.java +++ b/src/test/java/io/mvnpm/MavenRepositoryApiTest.java @@ -28,7 +28,7 @@ public class MavenRepositoryApiTest { @Test public void testBasicPom() { RestAssured.given().header("User-Agent", "m2e/unit-test") - .when().get("/maven2/org/mvnpm/lit/2.4.0/lit-2.4.0.pom") + .when().get("/maven2/org/mvnpm/lit/3.1.2/lit-3.1.2.pom") .then().log().all().and() .statusCode(200); } @@ -36,7 +36,7 @@ public void testBasicPom() { @Test public void testStarDependencyPom() { RestAssured.given().header("User-Agent", "m2e/unit-test") - .when().get("/maven2/org/mvnpm/at/types/codemirror/5.60.5/codemirror-5.60.5.pom") + .when().get("/maven2/org/mvnpm/at/types/codemirror/5.60.15/codemirror-5.60.15.pom") .then().log().all().and() .statusCode(200); } @@ -44,7 +44,7 @@ public void testStarDependencyPom() { @Test public void testIgnoreBetaPom() { RestAssured.given().header("User-Agent", "m2e/unit-test") - .when().get("/maven2/org/mvnpm/at/vaadin/tabs/23.2.3/vaadin-23.2.3.pom") + .when().get("/maven2/org/mvnpm/at/vaadin/tabs/24.3.8/vaadin-24.3.8.pom") .then().log().all().and() .statusCode(200); } @@ -77,11 +77,11 @@ public void testComposite() throws IOException { .setParam(CoreConnectionPNames.SO_TIMEOUT, 300000)); final byte[] jar = RestAssured.given().header("User-Agent", "m2e/unit-test") .config(config) - .when().get("/maven2/org/mvnpm/at/mvnpm/vaadin-webcomponents/24.2.5/vaadin-webcomponents-24.2.5.jar") + .when().get("/maven2/org/mvnpm/at/mvnpm/vaadin-webcomponents/24.3.8/vaadin-webcomponents-24.3.8.jar") .then() .statusCode(200) .extract().asByteArray(); - final Path tempFile = Files.createTempFile("vaadin-webcomponents-24.2.5", ".jar"); + final Path tempFile = Files.createTempFile("vaadin-webcomponents-24.3.8", ".jar"); final Path nodeModules = Files.createTempDirectory("node_modules"); Files.write(tempFile, jar); WebDepsInstaller.install(nodeModules,