From 132876c157c106adb9e8a631a4abeb4bc8540cab Mon Sep 17 00:00:00 2001 From: Walker Funk Date: Wed, 11 Sep 2019 14:13:48 -0400 Subject: [PATCH] RPM signing feature w/ added license info Signed-off by: Walker Funk --- .../META-INF/MANIFEST.MF | 7 +- .../OSGI-INF/rpmSign.properties | 5 + .../OSGI-INF/rpmSign.xml | 8 + .../repo/adapter/rpm/Constants.java | 5 + .../adapter/rpm/internal/RpmExtractor.java | 8 + .../rpm/internal/RpmSignerAspectFactory.java | 42 ++ .../rpm/internal/RpmSignerVirtualizer.java | 113 ++++++ .../yum/internal/YumChannelAggregator.java | 28 +- .../adapter/rpm/yum/internal/YumRecipe.java | 2 +- .../repo/channel/apm/ModifyContextImpl.java | 12 + .../repo/channel/impl/ChannelServiceImpl.java | 16 + .../META-INF/MANIFEST.MF | 4 + .../AbstractSecretKeySigningService.java | 19 + .../META-INF/MANIFEST.MF | 1 + .../repo/signing/SigningService.java | 5 + .../META-INF/MANIFEST.MF | 2 + .../utils/rpm/build/PayloadStreamer.java | 134 +++++++ .../utils/rpm/build/RpmRebuilder.java | 363 ++++++++++++++++++ .../utils/rpm/info/RpmInformations.java | 29 ++ .../utils/rpm/parse/RpmParserStream.java | 349 +++++++++++++++++ 20 files changed, 1139 insertions(+), 13 deletions(-) create mode 100644 bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.properties create mode 100644 bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.xml create mode 100644 bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerAspectFactory.java create mode 100644 bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerVirtualizer.java create mode 100644 bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/PayloadStreamer.java create mode 100644 bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/RpmRebuilder.java create mode 100644 bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/parse/RpmParserStream.java diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/META-INF/MANIFEST.MF b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/META-INF/MANIFEST.MF index e4604e6b..beae839c 100644 --- a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/META-INF/MANIFEST.MF @@ -8,16 +8,20 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Web-ContextPath: /yum Import-Package: com.google.common.escape;version="18.0.0", com.google.common.net;version="18.0.0", + com.google.common.io;version="18.0.0", + com.google.common.hash;version="18.0.0", com.google.gson;version="2.3.1", javax.servlet;version="3.1.0", javax.servlet.http;version="3.1.0", org.apache.commons.compress.archivers.cpio;version="1.9.0", + org.bouncycastle.openpgp;version="1.52.0", org.eclipse.packagedrone;version="1.0.0", org.eclipse.packagedrone.repo;version="1.0.0", org.eclipse.packagedrone.repo.aspect;version="1.0.0", org.eclipse.packagedrone.repo.aspect.aggregate;version="1.0.0", org.eclipse.packagedrone.repo.aspect.common.spool;version="1.0.0", org.eclipse.packagedrone.repo.aspect.extract;version="1.0.0", + org.eclipse.packagedrone.repo.aspect.virtual;version="1.0.0", org.eclipse.packagedrone.repo.aspect.group;version="1.0.0", org.eclipse.packagedrone.repo.aspect.recipe;version="1.0.0", org.eclipse.packagedrone.repo.channel;version="1.0.0", @@ -28,6 +32,7 @@ Import-Package: com.google.common.escape;version="18.0.0", org.eclipse.packagedrone.repo.utils;version="1.0.0", org.eclipse.packagedrone.repo.web.utils;version="1.0.0", org.eclipse.packagedrone.repo.xml;version="1.0.0", + org.eclipse.packagedrone.utils;version="1.0.0", org.eclipse.packagedrone.utils.io;version="1.0.0", org.eclipse.packagedrone.utils.rpm;version="0.13.0", org.eclipse.packagedrone.utils.rpm.deps;version="0.13.0", @@ -43,7 +48,7 @@ Import-Package: com.google.common.escape;version="18.0.0", org.osgi.framework;version="1.8.0", org.osgi.util.tracker;version="1.5.1", org.slf4j;version="1.7.2" -Service-Component: OSGI-INF/rpm.xml,OSGI-INF/yum.xml,OSGI-INF/groupRpm.xml,OSGI-INF/recipeYum.xml, +Service-Component: OSGI-INF/rpm.xml,OSGI-INF/rpmSign.xml,OSGI-INF/yum.xml,OSGI-INF/groupRpm.xml,OSGI-INF/recipeYum.xml, OSGI-INF/servlet.xml Require-Bundle: org.jboss.spec.javax.servlet.jstl.jboss-jstl-api_1.2_spec;bundle-version="1.1.2" Export-Package: org.eclipse.packagedrone.repo.adapter.rpm;version="1.0.0" diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.properties b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.properties new file mode 100644 index 00000000..d0afb5c5 --- /dev/null +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.properties @@ -0,0 +1,5 @@ +drone.aspect.id=rpm.signer +drone.aspect.name=RPM Signer +drone.aspect.description=Sign RPM files +drone.aspect.version=1.0.0 +drone.aspect.group.id=rpm \ No newline at end of file diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.xml b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.xml new file mode 100644 index 00000000..a22bd182 --- /dev/null +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/OSGI-INF/rpmSign.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/Constants.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/Constants.java index a9d1d0e6..74c6699d 100644 --- a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/Constants.java +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/Constants.java @@ -7,6 +7,7 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - rpm signing components *******************************************************************************/ package org.eclipse.packagedrone.repo.adapter.rpm; @@ -18,8 +19,12 @@ public final class Constants public static final String RPM_ASPECT_ID = "rpm"; + public static final String RPM_SIGN_ASPECT_ID = "rpm.signer"; + public static final String YUM_ASPECT_ID = "yum"; + public static final MetaKey KEY_RSA = new MetaKey ( GROUP_ID, "rsa" ); + public static final MetaKey KEY_INFO = new MetaKey ( RPM_ASPECT_ID, "info" ); private Constants () diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmExtractor.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmExtractor.java index db7eb8b3..d2bcbe55 100644 --- a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmExtractor.java +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmExtractor.java @@ -8,6 +8,7 @@ * Contributors: * IBH SYSTEMS GmbH - initial API and implementation * Bachmann electronic GmbH - #86 Adding 'release' rpm metadata tag + * Walker Funk - Trident Systems Inc. rpm rsa signature extraction *******************************************************************************/ package org.eclipse.packagedrone.repo.adapter.rpm.internal; @@ -21,6 +22,7 @@ import org.eclipse.packagedrone.repo.adapter.rpm.RpmInformationsJson; import org.eclipse.packagedrone.repo.aspect.extract.Extractor; import org.eclipse.packagedrone.utils.rpm.RpmTag; +import org.eclipse.packagedrone.utils.rpm.RpmSignatureTag; import org.eclipse.packagedrone.utils.rpm.info.RpmInformation; import org.eclipse.packagedrone.utils.rpm.info.RpmInformations; import org.eclipse.packagedrone.utils.rpm.parse.RpmInputStream; @@ -54,6 +56,12 @@ public void extractMetaData ( final Context context, final Map m metadata.put ( "release", RpmInformations.asString ( in.getPayloadHeader ().getTag ( RpmTag.RELEASE ) ) ); metadata.put ( Constants.KEY_INFO.getKey (), RpmInformationsJson.toJson ( info ) ); + + String signature = RpmInformations.asArmored ( in.getSignatureHeader ().getTag ( RpmSignatureTag.RSAHEADER ) ); + if ( signature != null && signature != "" ) + { + metadata.put ( Constants.KEY_RSA.getKey (), signature ); + } } catch ( final Exception e ) { diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerAspectFactory.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerAspectFactory.java new file mode 100644 index 00000000..4990765e --- /dev/null +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerAspectFactory.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2019 Trident Systems, Inc. + * This software was developed with U.S government funding in support of the above + * contract. Trident grants unlimited rights to modify, distribute and incorporate + * our contributions to Eclipse Package Drone bound by the overall restrictions from + * the parent Eclipse Public License v1.0 available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Walker Funk - Trident Systems Inc. - initial implementation + *******************************************************************************/ +package org.eclipse.packagedrone.repo.adapter.rpm.internal; + +import org.eclipse.packagedrone.repo.aspect.ChannelAspect; +import org.eclipse.packagedrone.repo.aspect.ChannelAspectFactory; +import org.eclipse.packagedrone.repo.aspect.virtual.Virtualizer; +import org.eclipse.packagedrone.repo.adapter.rpm.Constants; +import org.eclipse.packagedrone.repo.adapter.rpm.internal.RpmSignerVirtualizer; + +public class RpmSignerAspectFactory implements ChannelAspectFactory +{ + + @Override + public ChannelAspect createAspect () + { + return new ChannelAspect () { + + @Override + public String getId () + { + return Constants.RPM_SIGN_ASPECT_ID; + } + + @Override + public Virtualizer getArtifactVirtualizer () + { + return new RpmSignerVirtualizer (); + } + }; + } + +} diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerVirtualizer.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerVirtualizer.java new file mode 100644 index 00000000..a0b46aee --- /dev/null +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/internal/RpmSignerVirtualizer.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2019 Trident Systems, Inc. + * This software was developed with U.S government funding in support of the above + * contract. Trident grants unlimited rights to modify, distribute and incorporate + * our contributions to Eclipse Package Drone bound by the overall restrictions from + * the parent Eclipse Public License v1.0 available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Walker Funk - Trident Systems Inc. - initial implementation + *******************************************************************************/ +package org.eclipse.packagedrone.repo.adapter.rpm.internal; + +import java.io.DataInputStream; +import java.io.BufferedInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; + +import org.eclipse.packagedrone.repo.MetaKey; +import org.eclipse.packagedrone.repo.utils.HashHelper; +import org.eclipse.packagedrone.repo.signing.SigningService; +import org.eclipse.packagedrone.repo.aspect.virtual.Virtualizer; +import org.eclipse.packagedrone.repo.channel.ArtifactInformation; +import org.eclipse.packagedrone.repo.adapter.rpm.Constants; +import org.eclipse.packagedrone.repo.adapter.rpm.yum.internal.YumChannelAggregator; +import org.eclipse.packagedrone.utils.Exceptions; +import org.eclipse.packagedrone.utils.rpm.parse.RpmParserStream; + +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +import com.google.common.io.ByteStreams; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RpmSignerVirtualizer implements Virtualizer +{ + private final static Logger logger = LoggerFactory.getLogger ( RpmSignerVirtualizer.class ); + + private static final MetaKey KEY_SIGNING_ID = new MetaKey ( "yum", "signingServiceId" ); + + private final BundleContext bundleContext = FrameworkUtil.getBundle ( YumChannelAggregator.class ).getBundleContext (); + + @Override + public void virtualize ( final Context context ) + { + Exceptions.wrapException ( () -> processVirtualize ( context ) ); + } + + private void processVirtualize ( final Context context ) throws Exception + { + final Path path = context.getFile (); + final ArtifactInformation art = context.getArtifactInformation (); + final String name = art.getName (); + Map metaData = new HashMap<> ( art.getMetaData () ); + + if ( metaData.containsKey ( Constants.KEY_RSA ) ) + { + return; + } + + final String signingServiceId = context.getProvidedChannelMetaData ().get ( KEY_SIGNING_ID ); + ServiceReference ssref = null; + SigningService signingService = null; + + if ( signingServiceId != null && !signingServiceId.isEmpty () ) + { + final Collection> services = bundleContext.getServiceReferences ( SigningService.class, String.format( "(%s=%s)", org.osgi.framework.Constants.SERVICE_PID, signingServiceId ) ); + + if ( services == null || services.isEmpty () ) + { + throw new IllegalStateException ( String.format ( "Unable to find configured signing service: %s", signingServiceId ) ); + } + + ssref = services.iterator ().next (); + signingService = bundleContext.getService ( ssref ); + + try ( RpmParserStream preIn = new RpmParserStream ( new BufferedInputStream ( Files.newInputStream ( path, StandardOpenOption.READ ) ) ); ) + { + signingService.signRpm ( path, preIn ); + + Map functions = new HashMap<> (); + + functions.put ( "md5", Hashing.md5 () ); + functions.put ( "sha1", Hashing.sha1 () ); + functions.put ( "sha256", Hashing.sha256 () ); + functions.put ( "sha512", Hashing.sha512 () ); + + final Map result = HashHelper.createChecksums ( path, functions ); + for ( final Map.Entry entry : result.entrySet () ) + { + metaData.replace ( new MetaKey ("hasher", entry.getKey () ), entry.getValue ().toString () ); + } + + DataInputStream postIn = new DataInputStream ( Files.newInputStream( path, StandardOpenOption.READ ) ); + context.createVirtualArtifact ( name, out -> ByteStreams.copy ( postIn, out ), metaData ); + } + catch ( final Exception e ) + { + logger.debug ( "Failed to sign RPM", e ); + } + } + } +} diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumChannelAggregator.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumChannelAggregator.java index 884c0f24..f579496c 100644 --- a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumChannelAggregator.java +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumChannelAggregator.java @@ -7,6 +7,7 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - limit repo to only signed rpms when signing enabled *******************************************************************************/ package org.eclipse.packagedrone.repo.adapter.rpm.yum.internal; @@ -46,6 +47,8 @@ public class YumChannelAggregator implements ChannelAggregator private final XmlToolsFactory xml; + private boolean isSigning = false; + public YumChannelAggregator ( final XmlToolsFactory xml ) { this.xml = xml; @@ -69,6 +72,7 @@ public Map aggregateMetaData ( final AggregationContext context ssref = services.iterator ().next (); signingService = this.context.getService ( ssref ); + this.isSigning = true; } try @@ -88,20 +92,22 @@ public Map aggregateMetaData ( final AggregationContext context creator.process ( repoContext -> { for ( final ArtifactInformation art : context.getArtifacts () ) { - final RpmInformation info = RpmInformationsJson.fromJson ( art.getMetaData ().get ( Constants.KEY_INFO ) ); - - if ( info == null ) + if ( ( this.isSigning && art.getMetaData ().containsKey ( Constants.KEY_RSA ) ) || !this.isSigning ) { - continue; - } + final RpmInformation info = RpmInformationsJson.fromJson(art.getMetaData().get(Constants.KEY_INFO)); - final String sha1 = art.getMetaData ().get ( KEY_SHA1 ); - final Map checksums = Collections.singletonMap ( HashAlgorithm.SHA1, sha1 ); + if (info == null) { + continue; + } - final String location = String.format ( "pool/%s/%s", art.getId (), art.getName () ); - final RepositoryCreator.FileInformation file = new RepositoryCreator.FileInformation ( art.getCreationInstant (), art.getSize (), location ); + final String sha1 = art.getMetaData().get(KEY_SHA1); + final Map checksums = Collections.singletonMap(HashAlgorithm.SHA1, sha1); - repoContext.addPackage ( file, info, checksums, HashAlgorithm.SHA1 ); + final String location = String.format("pool/%s/%s", art.getId(), art.getName()); + final RepositoryCreator.FileInformation file = new RepositoryCreator.FileInformation(art.getCreationInstant(), art.getSize(), location); + + repoContext.addPackage(file, info, checksums, HashAlgorithm.SHA1); + } } } ); @@ -134,4 +140,4 @@ private DefaultXmlContext makeXmlContext () final DefaultXmlContext xmlCtx = new DefaultXmlContext ( dbf, this.xml.newTransformerFactory () ); return xmlCtx; } -} +} \ No newline at end of file diff --git a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumRecipe.java b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumRecipe.java index 4f071f81..454c3ca8 100644 --- a/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumRecipe.java +++ b/bundles/org.eclipse.packagedrone.repo.adapter.rpm/src/org/eclipse/packagedrone/repo/adapter/rpm/yum/internal/YumRecipe.java @@ -19,7 +19,7 @@ public class YumRecipe implements Recipe @Override public LinkTarget setup ( final String channelId, final AspectableChannel channel ) { - channel.addAspects ( true, "rpm", "yum" ); + channel.addAspects ( true, "rpm", "yum", "rpm.signer" ); return null; } } diff --git a/bundles/org.eclipse.packagedrone.repo.channel.apm/src/org/eclipse/packagedrone/repo/channel/apm/ModifyContextImpl.java b/bundles/org.eclipse.packagedrone.repo.channel.apm/src/org/eclipse/packagedrone/repo/channel/apm/ModifyContextImpl.java index 2ccd8098..5c5a599f 100644 --- a/bundles/org.eclipse.packagedrone.repo.channel.apm/src/org/eclipse/packagedrone/repo/channel/apm/ModifyContextImpl.java +++ b/bundles/org.eclipse.packagedrone.repo.channel.apm/src/org/eclipse/packagedrone/repo/channel/apm/ModifyContextImpl.java @@ -7,6 +7,7 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - added system call to clear function to delete empty directories *******************************************************************************/ package org.eclipse.packagedrone.repo.channel.apm; @@ -30,6 +31,7 @@ import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.lang.Runtime; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.channel.ArtifactInformation; @@ -822,6 +824,16 @@ public void clear () this.state.setNumberOfArtifacts ( 0L ); this.state.setNumberOfBytes ( 0L ); + + final String cmd = "rm -rf " + System.getProperty ( "drone.storage.base" ) + "/channels/" + this.channelId + "/blobs/data/*"; + try + { + Process p = Runtime.getRuntime().exec( new String[] {"sh", "-c", cmd} ); + } + catch ( Exception e ) + { + throw new RuntimeException ( "Could not remove channel " + this.channelId + " data directories" ); + } } @Override diff --git a/bundles/org.eclipse.packagedrone.repo.channel.impl/src/org/eclipse/packagedrone/repo/channel/impl/ChannelServiceImpl.java b/bundles/org.eclipse.packagedrone.repo.channel.impl/src/org/eclipse/packagedrone/repo/channel/impl/ChannelServiceImpl.java index 19c2d9d1..bdff4d3b 100644 --- a/bundles/org.eclipse.packagedrone.repo.channel.impl/src/org/eclipse/packagedrone/repo/channel/impl/ChannelServiceImpl.java +++ b/bundles/org.eclipse.packagedrone.repo.channel.impl/src/org/eclipse/packagedrone/repo/channel/impl/ChannelServiceImpl.java @@ -7,6 +7,7 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - added system call to delete function to delete empty directories *******************************************************************************/ package org.eclipse.packagedrone.repo.channel.impl; @@ -28,6 +29,10 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; import java.util.stream.Collectors; +import java.lang.Runtime; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.aspect.ChannelAspectProcessor; @@ -494,6 +499,17 @@ public boolean delete ( final By by ) channel.get ().delete (); + final String cmd = "rm -rf " + System.getProperty ( "drone.storage.base" ) + "/channels/" + instance.getChannelId (); + + try + { + Process p = Runtime.getRuntime().exec(cmd); + } + catch ( Exception e ) + { + throw new RuntimeException ( "Could not remove channel " + instance.getChannelId () + " files/directories" ); + } + return true; } } diff --git a/bundles/org.eclipse.packagedrone.repo.signing.pgp/META-INF/MANIFEST.MF b/bundles/org.eclipse.packagedrone.repo.signing.pgp/META-INF/MANIFEST.MF index 028ed19a..dbafd9bb 100644 --- a/bundles/org.eclipse.packagedrone.repo.signing.pgp/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.packagedrone.repo.signing.pgp/META-INF/MANIFEST.MF @@ -18,6 +18,10 @@ Import-Package: com.google.gson;version="2.3.1", org.eclipse.packagedrone.storage.apm;version="1.0.0", org.eclipse.packagedrone.storage.apm.util;version="1.0.0", org.eclipse.packagedrone.utils;version="1.0.0", + org.eclipse.packagedrone.utils.rpm;version="0.15.0", + org.eclipse.packagedrone.utils.rpm.build;version="0.15.0", + org.eclipse.packagedrone.utils.rpm.parse;version="0.15.0", + org.eclipse.packagedrone.utils.rpm.signature;version="0.15.0", org.eclipse.packagedrone.utils.security.pgp;version="0.14.0", org.eclipse.scada.utils;version="0.2.0", org.osgi.framework;version="1.8.0", diff --git a/bundles/org.eclipse.packagedrone.repo.signing.pgp/src/org/eclipse/packagedrone/repo/signing/pgp/internal/AbstractSecretKeySigningService.java b/bundles/org.eclipse.packagedrone.repo.signing.pgp/src/org/eclipse/packagedrone/repo/signing/pgp/internal/AbstractSecretKeySigningService.java index 4fb3d506..58527d1f 100644 --- a/bundles/org.eclipse.packagedrone.repo.signing.pgp/src/org/eclipse/packagedrone/repo/signing/pgp/internal/AbstractSecretKeySigningService.java +++ b/bundles/org.eclipse.packagedrone.repo.signing.pgp/src/org/eclipse/packagedrone/repo/signing/pgp/internal/AbstractSecretKeySigningService.java @@ -7,6 +7,7 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - rpm signing components *******************************************************************************/ package org.eclipse.packagedrone.repo.signing.pgp.internal; @@ -16,6 +17,7 @@ import java.io.LineNumberReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGOutputStream; @@ -32,6 +34,12 @@ import org.eclipse.packagedrone.VersionInformation; import org.eclipse.packagedrone.repo.signing.SigningService; import org.eclipse.packagedrone.utils.security.pgp.SigningStream; +import org.eclipse.packagedrone.utils.rpm.signature.RsaHeaderSignatureProcessor; +import org.eclipse.packagedrone.utils.rpm.signature.PgpHeaderSignatureProcessor; +import org.eclipse.packagedrone.utils.rpm.signature.SignatureProcessors; +import org.eclipse.packagedrone.utils.rpm.build.RpmRebuilder; +import org.eclipse.packagedrone.utils.rpm.HashAlgorithm; +import org.eclipse.packagedrone.utils.rpm.parse.RpmParserStream; public abstract class AbstractSecretKeySigningService implements SigningService { @@ -125,6 +133,17 @@ public void sign ( final InputStream in, final OutputStream out, final boolean i armoredOutput.close (); } + @Override + public void signRpm ( Path path, RpmParserStream in ) throws Exception + { + RsaHeaderSignatureProcessor rhsp = new RsaHeaderSignatureProcessor ( this.privateKey, HashAlgorithm.SHA256 ); + RpmRebuilder rpmRebuilder = new RpmRebuilder ( path, in.getLead (), in.getSignatureHeader (), in.getPayloadHeader () ); + rpmRebuilder.setPayload ( in.getPayloadStreamer () ); + rpmRebuilder.addSignatureProcessor ( rhsp ); + in.close (); + rpmRebuilder.close (); + } + private static String trimTrailing ( final String line ) { final char[] content = line.toCharArray (); diff --git a/bundles/org.eclipse.packagedrone.repo.signing/META-INF/MANIFEST.MF b/bundles/org.eclipse.packagedrone.repo.signing/META-INF/MANIFEST.MF index ef75b0b4..9aef462f 100644 --- a/bundles/org.eclipse.packagedrone.repo.signing/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.packagedrone.repo.signing/META-INF/MANIFEST.MF @@ -5,4 +5,5 @@ Bundle-SymbolicName: org.eclipse.packagedrone.repo.signing Bundle-Version: 1.0.0.qualifier Bundle-Vendor: Jens Reimann Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Import-Package: org.eclipse.packagedrone.utils.rpm.parse;version="0.15.0" Export-Package: org.eclipse.packagedrone.repo.signing;version="1.0.0" diff --git a/bundles/org.eclipse.packagedrone.repo.signing/src/org/eclipse/packagedrone/repo/signing/SigningService.java b/bundles/org.eclipse.packagedrone.repo.signing/src/org/eclipse/packagedrone/repo/signing/SigningService.java index 8937f7dd..2c5a48ee 100644 --- a/bundles/org.eclipse.packagedrone.repo.signing/src/org/eclipse/packagedrone/repo/signing/SigningService.java +++ b/bundles/org.eclipse.packagedrone.repo.signing/src/org/eclipse/packagedrone/repo/signing/SigningService.java @@ -7,17 +7,22 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - rpm signing components *******************************************************************************/ package org.eclipse.packagedrone.repo.signing; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Path; +import org.eclipse.packagedrone.utils.rpm.parse.RpmParserStream; public interface SigningService { public void sign ( InputStream in, OutputStream out, boolean inline ) throws Exception; + public void signRpm ( Path path, RpmParserStream in ) throws Exception; + public void printPublicKey ( OutputStream out ) throws IOException; public OutputStream signingStream ( OutputStream out, boolean inline ); diff --git a/bundles/org.eclipse.packagedrone.utils.rpm/META-INF/MANIFEST.MF b/bundles/org.eclipse.packagedrone.utils.rpm/META-INF/MANIFEST.MF index 2d1ef5e1..37cd51af 100644 --- a/bundles/org.eclipse.packagedrone.utils.rpm/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.packagedrone.utils.rpm/META-INF/MANIFEST.MF @@ -27,7 +27,9 @@ Import-Package: com.google.common.io;version="18.0.0", org.apache.commons.compress.utils;version="1.18.0", org.bouncycastle.bcpg;version="1.52.0", org.bouncycastle.openpgp;version="1.52.0", + org.bouncycastle.openpgp.operator;version="1.52.0", org.bouncycastle.openpgp.operator.bc;version="1.52.0", + org.eclipse.packagedrone;version="1.0.0", org.eclipse.packagedrone.utils.io;version="1.0.0", org.eclipse.packagedrone.utils.security.pgp;version="0.14.0", org.slf4j;version="1.7.2", diff --git a/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/PayloadStreamer.java b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/PayloadStreamer.java new file mode 100644 index 00000000..749512b6 --- /dev/null +++ b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/PayloadStreamer.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2019 Trident Systems, Inc. + * This software was developed with U.S government funding in support of the above + * contract. Trident grants unlimited rights to modify, distribute and incorporate + * our contributions to Eclipse Package Drone bound by the overall restrictions from + * the parent Eclipse Public License v1.0 available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Walker Funk - Trident Systems Inc. - initial implementation + *******************************************************************************/ +package org.eclipse.packagedrone.utils.rpm.build; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.DataInputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import com.google.common.io.ByteStreams; +import com.google.common.io.CountingOutputStream; + +public class PayloadStreamer implements AutoCloseable +{ + private final boolean autoFinish; + + private final Path tempFile; + + private final CountingOutputStream payloadCounter; + + private OutputStream fileStream; + + private boolean finished; + + private boolean closed; + + public PayloadStreamer ( final boolean autoFinish, DataInputStream in ) throws IOException + { + this.autoFinish = autoFinish; + + this.tempFile = Files.createTempFile ( "rpm-", null ); + + try + { + this.fileStream = new BufferedOutputStream ( Files.newOutputStream ( this.tempFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING ) ); + + this.payloadCounter = new CountingOutputStream ( this.fileStream ); + + ByteStreams.copy( in, payloadCounter ); + } + catch ( final IOException e ) + { + Files.deleteIfExists ( this.tempFile ); + throw e; + } + } + + /** + * Stop streaming payload data + *

+ * If the streamer is already finished then nothing will happen + *

+ * + * @throws IOException + * in case of IO errors + */ + public void finish () throws IOException + { + if ( this.finished ) + { + return; + } + + this.finished = true; + + this.payloadCounter.close (); + } + + public long getPayloadSize () throws IOException + { + checkFinished ( true ); + + return this.payloadCounter.getCount (); + } + + public FileChannel openChannel () throws IOException + { + checkFinished ( true ); + + return FileChannel.open ( this.tempFile, StandardOpenOption.READ ); + } + + private void checkFinished ( final boolean allowAutoFinish ) throws IOException + { + if ( !this.finished && this.autoFinish && allowAutoFinish ) + { + finish (); + } + + if ( !this.finished ) + { + throw new IllegalStateException ( "Recoderd has to be finished before accessing payload information or data" ); + } + if ( this.closed ) + { + throw new IllegalStateException ( "Recorder is already closed" ); + } + } + + @Override + public void close () throws IOException + { + this.closed = true; + + try + { + // simply close the file stream + + if ( this.fileStream != null ) + { + this.fileStream.close (); + } + } + finally + { + // and delete the temp file + + Files.deleteIfExists ( this.tempFile ); + } + } +} diff --git a/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/RpmRebuilder.java b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/RpmRebuilder.java new file mode 100644 index 00000000..22c6ad9c --- /dev/null +++ b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/build/RpmRebuilder.java @@ -0,0 +1,363 @@ +/******************************************************************************* + * Copyright (c) 2019 Trident Systems, Inc. + * This software was developed with U.S government funding in support of the above + * contract. Trident grants unlimited rights to modify, distribute and incorporate + * our contributions to Eclipse Package Drone bound by the overall restrictions from + * the parent Eclipse Public License v1.0 available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Walker Funk - Trident Systems Inc. - initial implementation + *******************************************************************************/ +package org.eclipse.packagedrone.utils.rpm.build; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.LinkedList; +import java.util.List; + +import org.eclipse.packagedrone.utils.rpm.RpmLead; +import org.eclipse.packagedrone.utils.rpm.RpmSignatureTag; +import org.eclipse.packagedrone.utils.rpm.RpmTag; +import org.eclipse.packagedrone.utils.rpm.Rpms; +import org.eclipse.packagedrone.utils.rpm.header.Header; +import org.eclipse.packagedrone.utils.rpm.header.Headers; +import org.eclipse.packagedrone.utils.rpm.signature.SignatureProcessor; +import com.google.common.io.ByteStreams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A low level RPM file rebuilder + *

+ * This class handles the reconstructing of RPM files. It does not really care about the + * contents it writes but the content and the format of the content should come + * from a pre-parsed rpm (ideally via the RpmParserStreamer class) and + * added signature processor(s) + *

+ */ +public class RpmRebuilder implements AutoCloseable +{ + private static final OpenOption[] DEFAULT_OPEN_OPTIONS = new OpenOption[] { StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING }; + + private final static Logger logger = LoggerFactory.getLogger ( RpmRebuilder.class ); + + private final FileChannel file; + + private final RpmLead lead; + + private final Header signatureHeader; + + private final ByteBuffer header; + + private boolean finished; + + private PayloadStreamer payloadStreamer; + + private final List signatureProcessors = new LinkedList<> (); + + public RpmRebuilder ( final Path path, final RpmLead lead, final Header signatureHeader, final Header header ) throws IOException + { + requireNonNull ( path ); + requireNonNull ( lead ); + requireNonNull ( header ); + requireNonNull ( signatureHeader ); + + this.file = FileChannel.open ( path, DEFAULT_OPEN_OPTIONS ); + this.lead = lead; + this.signatureHeader = signatureHeader; + + this.header = Headers.render ( header.makeEntries (), true, Rpms.IMMUTABLE_TAG_HEADER ); + } + + public void addSignatureProcessor ( final SignatureProcessor processor ) + { + this.signatureProcessors.add ( processor ); + } + + public void addAllSignatureProcessors ( final List signatureProcessors ) + { + this.signatureProcessors.addAll ( signatureProcessors ); + } + + public void setPayload ( final PayloadStreamer payloadStreamer ) throws IOException + { + checkNotFinished (); + + requireNonNull ( payloadStreamer ); + + this.payloadStreamer = payloadStreamer; + } + + private void checkNotFinished () + { + if ( this.finished ) + { + throw new IllegalStateException ( "Writing of RPM is already finished" ); + } + } + + private static void debug ( final String fmt, final Object... args ) + { + logger.debug ( String.format ( fmt, args ) ); + } + + private void writeLead () throws IOException + { + // write lead + + final ByteBuffer lead = ByteBuffer.allocate ( Rpms.LEAD_MAGIC.length + 2 + 4 + 66 + 2 + 2 + 16 ); + + lead.put ( Rpms.LEAD_MAGIC ); + lead.put ( this.lead.getMajor () ); + lead.put ( this.lead.getMinor () ); + + // 2 bytes type + + lead.putShort ( this.lead.getType () ); + + // 2 bytes arch + + lead.putShort ( this.lead.getArchitecture () ); + + // write package name + + { + final ByteBuffer nameData = StandardCharsets.UTF_8.encode ( this.lead.getName () ); + final int len = nameData.remaining (); + if ( len > 66 ) + { + throw new IOException ( "Name exceeds 66 bytes" ); + } + + lead.put ( nameData ); + if ( len < 66 ) + { + // fill up + lead.put ( Rpms.EMPTY_128, 0, 66 - len ); + } + } + + // 2 bytes OS + + lead.putShort ( this.lead.getOperatingSystem () ); + + // 2 bytes signature + + lead.putShort ( (short)this.lead.getSignatureVersion () ); + + // 16 bytes reserved + + lead.put ( Rpms.EMPTY_128, 0, 16 ); + + lead.flip (); + safeWrite ( lead ); + } + + private void safeWrite ( final ByteBuffer data ) throws IOException + { + while ( data.hasRemaining () ) + { + this.file.write ( data ); + } + } + + private void writeSignatureHeader ( final Header header ) throws IOException + { + // render header + + final ByteBuffer buffer = Headers.render ( header.makeEntries (), true, Rpms.IMMUTABLE_TAG_SIGNATURE ); + + final int payloadSize = buffer.remaining (); + + // header + + debug ( "start header - offset: %s, len: %s", this.file.position (), payloadSize ); + safeWrite ( buffer ); + + // padding + + final int padding = Rpms.padding ( payloadSize ); + + if ( padding > 0 ) + { + safeWrite ( ByteBuffer.wrap ( Rpms.EMPTY_128, 0, padding ) ); + debug ( "write - padding - %s", padding ); + } + } + + @Override + public void close () throws IOException + { + try + { + finish (); + } + finally + { + this.file.close (); + this.payloadStreamer.close (); + } + } + + private void finish () throws IOException + { + if ( this.finished ) + { + return; + } + + if ( this.payloadStreamer == null ) + { + throw new IOException ( "Unable to finish RPM file, payload provider not set" ); + } + + this.finished = true; + + final int headerSize = this.header.remaining (); + final long payloadSize = this.payloadStreamer.getPayloadSize (); + debug ( "data - %s - %s", headerSize, payloadSize ); + + // process signatures + + processSignatures ( this.signatureHeader ); + + // write lead + + writeLead (); + + // write signature header + + writeSignatureHeader ( this.signatureHeader ); + + // write the header + + debug ( "package - offset: %s", this.file.position () ); + safeWrite ( this.header.slice () ); // write sliced to keep the original position + + debug ( "payload - offset: %s", this.file.position () ); + + // now append payload data + + try ( ReadableByteChannel payloadChannel = this.payloadStreamer.openChannel () ) + { + if ( payloadChannel instanceof FileChannel && !isForceCopy () ) + { + final long count = copyFileChannel ( (FileChannel)payloadChannel, this.file ); + debug ( "transfered - %s", count ); + } + else + { + final long count = ByteStreams.copy ( payloadChannel, this.file ); + debug ( "copyied - %s", count ); + } + } + + debug ( "end - offset: %s", this.file.position () ); + } + + /** + * Check of in-JVM copy should be forced over + * {@link FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)} + * + * @return {@code true} if copying should be forced, {@code false} + * otherwise. Defaults to {@code false}. + */ + private static boolean isForceCopy () + { + return Boolean.getBoolean ( "org.eclipse.packagedrone.utils.rpm.build.RpmRebuilder.forceCopy" ); + } + + private static long copyFileChannel ( final FileChannel fileChannel, final FileChannel file ) throws IOException + { + long remaning = fileChannel.size (); + long position = 0; + + while ( remaning > 0 ) + { + // transfer next chunk + + final long rc = fileChannel.transferTo ( position, remaning, file ); + + // check for negative result + + if ( rc < 0 ) + { + throw new IOException ( String.format ( "Failed to transfer bytes: rc = %s", rc ) ); + } + + debug ( "transferTo - position: %s, size: %s => rc: %s", position, remaning, rc ); + + // we should never get zero back, but check anyway + + if ( rc == 0 ) + { + break; + } + + // update state + + position += rc; + remaning -= rc; + } + + // final check if we got it all + + if ( remaning > 0 ) + { + throw new IOException ( "Failed to transfer full content" ); + } + + return position; + } + + private void processSignatures ( final Header signature ) throws IOException + { + // init + + // for ( final SignatureProcessor processor : this.signatureProcessors ) + // { + // processor.init ( this.payloadStreamer.getArchiveSize () ); + // } + + // feed the header + + for ( final SignatureProcessor processor : this.signatureProcessors ) + { + processor.feedHeader ( this.header.slice () ); + } + + // feed payload data + + try ( ReadableByteChannel channel = this.payloadStreamer.openChannel () ) + { + final ByteBuffer buf = ByteBuffer.wrap ( new byte[4096] ); + + while ( channel.read ( buf ) >= 0 ) + { + buf.flip (); + for ( final SignatureProcessor processor : this.signatureProcessors ) + { + processor.feedPayloadData ( buf.slice () ); + } + buf.clear (); + } + } + + // finish up + + for ( final SignatureProcessor processor : this.signatureProcessors ) + { + processor.finish ( signature ); + } + } + +} diff --git a/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/info/RpmInformations.java b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/info/RpmInformations.java index 2cb142f8..59de29bf 100644 --- a/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/info/RpmInformations.java +++ b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/info/RpmInformations.java @@ -7,10 +7,12 @@ * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation + * Walker Funk - Trident Systems Inc. - asArmored method for armoring signature data *******************************************************************************/ package org.eclipse.packagedrone.utils.rpm.info; import java.io.IOException; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -20,11 +22,14 @@ import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry; import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream; +import org.eclipse.packagedrone.VersionInformation; import org.eclipse.packagedrone.utils.rpm.RpmSignatureTag; import org.eclipse.packagedrone.utils.rpm.RpmTag; import org.eclipse.packagedrone.utils.rpm.info.RpmInformation.Dependency; import org.eclipse.packagedrone.utils.rpm.parse.InputHeader; import org.eclipse.packagedrone.utils.rpm.parse.RpmInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; public final class RpmInformations { @@ -217,6 +222,30 @@ public static String normalize ( final String name ) return name; } + public static String asArmored ( final Object value ) throws PGPException + { + if ( value == null ) + { + return null; + } + try + { + byte[] castVal = ( byte[] ) value; + ByteArrayOutputStream baos = new ByteArrayOutputStream (); + ArmoredOutputStream aos = new ArmoredOutputStream ( baos ); + aos.setHeader ( "Version", VersionInformation.VERSIONED_PRODUCT ); + + aos.write( castVal ); + aos.flush (); + aos.close (); + return new String ( baos.toByteArray () ); + } + catch ( Exception e ) + { + throw new PGPException ( "Error armoring signature data", e ); + } + } + public static String asString ( final Object value ) { if ( value == null ) diff --git a/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/parse/RpmParserStream.java b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/parse/RpmParserStream.java new file mode 100644 index 00000000..df3108a8 --- /dev/null +++ b/bundles/org.eclipse.packagedrone.utils.rpm/src/org/eclipse/packagedrone/utils/rpm/parse/RpmParserStream.java @@ -0,0 +1,349 @@ +/******************************************************************************* + * Copyright (c) 2019 Trident Systems, Inc. + * This software was developed with U.S government funding in support of the above + * contract. Trident grants unlimited rights to modify, distribute and incorporate + * our contributions to Eclipse Package Drone bound by the overall restrictions from + * the parent Eclipse Public License v1.0 available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Walker Funk - Trident Systems Inc. - initial implementation + *******************************************************************************/ +package org.eclipse.packagedrone.utils.rpm.parse; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.eclipse.packagedrone.utils.rpm.RpmBaseTag; +import org.eclipse.packagedrone.utils.rpm.RpmLead; +import org.eclipse.packagedrone.utils.rpm.RpmSignatureTag; +import org.eclipse.packagedrone.utils.rpm.RpmTag; +import org.eclipse.packagedrone.utils.rpm.Rpms; +import org.eclipse.packagedrone.utils.rpm.header.*; +import org.eclipse.packagedrone.utils.rpm.build.PayloadStreamer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.common.io.CountingInputStream; + +public class RpmParserStream extends InputStream +{ + private final static Logger logger = LoggerFactory.getLogger ( RpmParserStream.class ); + + private static final byte[] DUMMY = new byte[128]; + + private final DataInputStream in; + + private boolean closed; + + private RpmLead lead; + + private Header signatureHeader; + + private Header payloadHeader; + + private PayloadStreamer payloadStreamer; + + private final CountingInputStream count; + + public RpmParserStream ( final InputStream in ) + { + this.count = new CountingInputStream ( in ); + this.in = new DataInputStream ( this.count ); + } + + @Override + public void close () throws IOException + { + if ( !this.closed ) + { + this.in.close (); + this.closed = true; + } + } + + protected void ensureInit () throws IOException + { + if ( this.lead == null ) + { + this.lead = readLead (); + } + + if ( this.signatureHeader == null ) + { + this.signatureHeader = readHeader ( true ); + } + + if ( this.payloadHeader == null ) + { + this.payloadHeader = readHeader ( false ); + } + + // set up content stream + if ( this.payloadStreamer == null ) + { + this.payloadStreamer = new PayloadStreamer( true, this.in ); + } + } + + public PayloadStreamer getPayloadStreamer () + { + return this.payloadStreamer; + } + + public RpmLead getLead () throws IOException + { + ensureInit (); + return this.lead; + } + + public Header getSignatureHeader () throws IOException + { + ensureInit (); + return this.signatureHeader; + } + + public Header getPayloadHeader () throws IOException + { + ensureInit (); + return this.payloadHeader; + } + + protected RpmLead readLead () throws IOException + { + final byte[] magic = readComplete ( 4 ); + + if ( !Arrays.equals ( magic, Rpms.LEAD_MAGIC ) ) + { + throw new IOException ( String.format ( "File corrupt: Expected magic %s, read: %s", Arrays.toString ( Rpms.LEAD_MAGIC ), Arrays.toString ( magic ) ) ); + } + + final byte[] version = readComplete ( 2 ); + + final short type = this.in.readShort (); + final short arch = this.in.readShort (); + + final byte[] nameData = readComplete ( 66 ); // NAME + + final ByteBuffer nameBuffer = ByteBuffer.wrap ( nameData ); + for ( int i = 0; i < nameData.length; i++ ) + { + if ( nameData[i] == 0 ) + { + nameBuffer.limit ( i ); + break; + } + } + + final String name = StandardCharsets.UTF_8.decode ( nameBuffer ).toString (); + + final short os = this.in.readShort (); + + final int sigType = this.in.readUnsignedShort (); + + skipFully ( 16 ); // RESERVED + + return new RpmLead ( version[0], version[1], name, sigType, type, arch, os ); + } + + protected Header readHeader ( final boolean withPadding ) throws IOException + { + final long start = this.count.getCount (); + + final byte[] magic = readComplete ( 3 ); + + if ( !Arrays.equals ( magic, Rpms.HEADER_MAGIC ) ) + { + throw new IOException ( String.format ( "File corrupt: Expected entry magic %s, read: %s", Arrays.toString ( Rpms.HEADER_MAGIC ), Arrays.toString ( magic ) ) ); + } + + final byte version = this.in.readByte (); + if ( version != 1 ) + { + throw new IOException ( String.format ( "File corrupt: Invalid header entry version: %s (valid: 1)", version ) ); + } + + skipFully ( 4 ); // RESERVED + + final int indexCount = (this.in.readInt () - 1); // Subtract one to account for immutable + final int storeSize = this.in.readInt (); + + skipFully ( 16 ); // IMMUTABLE + + final ByteBuffer tags = ByteBuffer.wrap ( readComplete ( (indexCount * 16) ) ); + final ByteBuffer store = ByteBuffer.wrap ( readComplete ( storeSize ) ); + + final HeaderEntry[] entries = new HeaderEntry[indexCount]; + for ( int i = 0; i < indexCount; i++ ) + { + entries[i] = readEntry ( tags, store ); + } + + if ( withPadding ) + { + // pad remaining bytes - to 8 + + final int skip = Rpms.padding ( storeSize ); + if ( skip > 0 ) + { + logger.debug ( "Skipping {} pad bytes", skip ); + skipFully ( skip ); + } + } + + return new Header<> ( entries ); + } + + private HeaderEntry readEntry ( ByteBuffer tags, ByteBuffer store ) throws IOException + { + final int tag = tags.getInt (); + final int dataType = tags.getInt (); + final int offset = tags.getInt (); + final int count = tags.getInt (); + + + final Type type; + byte[] data = null; + switch ( dataType ) + { + case 1: + type = Type.CHAR; + data = new byte[count]; + store.position ( offset ); + store.get ( data ); + break; + case 2: + type = Type.BYTE; + data = new byte[count]; + store.position ( offset ); + store.get ( data ); + break; + case 3: + type = Type.SHORT; + data = new byte[2 * count]; + store.position ( offset ); + store.get ( data ); + break; + case 4: + type = Type.INT; + data = new byte[4 * count]; + store.position ( offset ); + store.get ( data ); + break; + case 5: + type = Type.LONG; + data = new byte[8 * count]; + store.position ( offset ); + store.get ( data ); + break; + case 6: + type = Type.STRING; + data = getBinStringData ( store, offset, 1 ); + break; + case 7: + type = Type.BLOB; + data = new byte[count]; + store.position ( offset ); + store.get ( data ); + break; + case 8: + type = Type.STRING_ARRAY; + data = getBinStringData ( store, offset, count ); + break; + case 9: + type = Type.I18N_STRING; + data = getBinStringData ( store, offset, count ); + break; + default: + type = Type.NULL; // Null or Unknown (both unsupported) + break; + } + if (data == null) + { + throw new IOException ( "Null or unknown data type" ); + } + return new HeaderEntry ( type, tag, count, data ); + } + + private byte[] getBinStringData ( final ByteBuffer store, int offset, final int strings ) throws IOException + { + int num = 0; + store.position ( offset ); + for ( int i = 0; i < store.remaining (); i++ ) // Find null terminated string or sequence of null terminated strings + { + if ( store.get ( offset + i ) == 0 ) + { + num++; + } + if ( num == strings ) + { + byte[] data = new byte[i + 1]; + store.get ( data ); + return data; + } + } + throw new IOException ( "Corrupt tag entry" ); + } + + private byte[] readComplete ( final int size ) throws IOException + { + final byte[] result = new byte[size]; + this.in.readFully ( result ); + return result; + } + + private void skipFully ( final int count ) throws IOException + { + if ( count > 0 ) + { + this.in.readFully ( DUMMY, 0, count ); + } + } + + // forward methods + + @Override + public void reset () throws IOException + { + ensureInit (); + this.in.reset (); + } + + @Override + public int read () throws IOException + { + ensureInit (); + return this.in.read (); + } + + @Override + public long skip ( final long n ) throws IOException + { + ensureInit (); + return this.in.skip ( n ); + } + + @Override + public int available () throws IOException + { + ensureInit (); + return this.in.available (); + } + + @Override + public int read ( final byte[] b ) throws IOException + { + ensureInit (); + return this.in.read ( b ); + } + + @Override + public int read ( final byte[] b, final int off, final int len ) throws IOException + { + return this.in.read ( b, off, len ); + } + +}