Skip to content

Commit

Permalink
OAK-6575: BlobURIProvider instead of OakConversionService on top of t…
Browse files Browse the repository at this point in the history
…he whiteboard instead of adapter manager
  • Loading branch information
mduerig committed Sep 6, 2017
1 parent 3c2eb6d commit 2709c09
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/

package org.apache.jackrabbit.oak.api.conversion;

import java.net.URI;

import org.apache.jackrabbit.oak.api.Blob;

public interface BlobURIProvider {
URI getURI(Blob blob);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
@Version("1.0.0")
package org.apache.jackrabbit.oak.api.conversion;

import org.osgi.annotation.versioning.Version;
11 changes: 11 additions & 0 deletions oak-blob-cloud/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.24</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-cloudfront</artifactId>
<version>1.11.24</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
Expand Down Expand Up @@ -171,6 +177,11 @@
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/


package org.apache.jackrabbit.oak.blob.cloud.aws.s3;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nonnull;

import com.amazonaws.services.cloudfront.CloudFrontUrlSigner;
import org.apache.commons.codec.binary.Base64;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Property;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.conversion.BlobURIProvider;
import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Adapts from Value to URI where Value has a Binary value.
* If running as an OSGi Component would expect an OSGi AdapterManager to pick it up.
* other.
*
* To generate keys in PKCS8 format use OpenSSL
* openssl genrsa -out private_key.pem 1024
* openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private_key.pem -out private_key.pkcs8
* See http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs for
* details on how to configure CloudFront.
*/
@Component(immediate = true, metatype = true)
public class CloudFrontS3SignedUrlAdapterFactory implements BlobURIProvider {
private static final String[] TARGET_CLASSES = new String[]{URI.class.getName()};

@Property
public static final String CLOUD_FRONT_URL = "cloudFrontUrl";
@Property(intValue = 60)
public static final String TTL = "ttl";
@Property
public static final String PRIVATE_KEY = "privateKey";
@Property
public static final String KEY_PAIR_ID = "keyPairId";
public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n";
public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";

private static final Logger LOGGER = LoggerFactory.getLogger(CloudFrontS3SignedUrlAdapterFactory.class);
private Whiteboard whiteboard;
private String cloudFrontUrl;
private long ttl;
private String keyPairId;
private RSAPrivateKey privateKey;

/**
* Default Constructor used by OSGi.
*/
public CloudFrontS3SignedUrlAdapterFactory() {
}

/**
* Non OSGi IoC constructor, close must be called when done.
* @param whiteboard
* @param cloudFrontUrl
* @param ttl
* @param privateKeyPEM
* @param privateKeyId
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
public CloudFrontS3SignedUrlAdapterFactory(Whiteboard whiteboard,
String cloudFrontUrl,
long ttl,
String privateKeyPEM,
String privateKeyId) throws InvalidKeySpecException, NoSuchAlgorithmException {
this.whiteboard = whiteboard;
init(cloudFrontUrl, ttl, privateKeyPEM, privateKeyId);
}

public void close() {
deactivate(new HashMap<String, Object>());
}

@Deactivate
public void deactivate(Map<String, Object> properties) {
}


@Activate
public void activate(BundleContext bundleContext, Map<String, Object> properties) throws InvalidKeySpecException, NoSuchAlgorithmException {
init((String) properties.get(CLOUD_FRONT_URL),
Long.parseLong((String) properties.get(TTL)),
(String) properties.get(PRIVATE_KEY),
(String) properties.get(KEY_PAIR_ID));
whiteboard = new OsgiWhiteboard(bundleContext);
whiteboard.register(BlobURIProvider.class, this, Collections.emptyMap());
}

private void init(String cloudFrontUrl, long ttl, String privateKeyPEM, String privateKeyId) throws InvalidKeySpecException, NoSuchAlgorithmException {
this.cloudFrontUrl = cloudFrontUrl;
this.ttl = ttl;
this.privateKey = getPrivateKey(privateKeyPEM);
this.keyPairId = privateKeyId;
}

@Override
public URI getURI(Blob blob) {
try {
String contentId = blob.getContentIdentity();
if ( contentId != null ) {
// could get the cloudFrontUrl, keyParId and private key based on the resource
// so that multiple S3 stores or even multiple CDNs could be used, but for this PoC keeping it simple.
return new URI(signS3Url(contentId, ttl, cloudFrontUrl, keyPairId, privateKey));
}
} catch (Exception e) {
LOGGER.error("Unable to get or sign content identity",e);
}

return null;
}

/**
* Convert the content identiry to an S3 url, see the oak blob-cloud code for how this is done.
* See S3Backend class in oak blob cloud package.
* @param contentIdentity
* @return
*/
@Nonnull
private String getS3Key(@Nonnull String contentIdentity) {
return contentIdentity.substring(0, 4) + "-" + contentIdentity.substring(4);
}

@Nonnull
private String signS3Url(@Nonnull String contentIdentity, long ttl, @Nonnull String cloudFrontUrl,
@Nonnull String keyPairId, @Nonnull RSAPrivateKey privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {

long expiry = (System.currentTimeMillis()/1000)+ttl;
StringBuilder urlToSign = new StringBuilder();

urlToSign.append(cloudFrontUrl)
.append(getS3Key(contentIdentity));
return CloudFrontUrlSigner.getSignedURLWithCannedPolicy(urlToSign.toString(), keyPairId, privateKey, new Date(expiry));
}


private RSAPrivateKey getPrivateKey(String privateKeyPKCS8) throws NoSuchAlgorithmException, InvalidKeySpecException {
int is = privateKeyPKCS8.indexOf(BEGIN_PRIVATE_KEY);
int ie = privateKeyPKCS8.indexOf(END_PRIVATE_KEY);
if (ie < 0 || is < 0) {
throw new IllegalArgumentException("Private Key is not correctly encoded, need a PEM encoded key with " +
"-----BEGIN PRIVATE KEY----- headers to indicate PKCS8 encoding.");
}
privateKeyPKCS8 = privateKeyPKCS8.substring(is+BEGIN_PRIVATE_KEY.length(),ie).trim();
byte[] privateKeyBytes = Base64.decodeBase64(privateKeyPKCS8);

// load the private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec ks = new PKCS8EncodedKeySpec(privateKeyBytes);
return (RSAPrivateKey) keyFactory.generatePrivate(ks);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/

package org.apache.jackrabbit.oak.coversion;

import java.net.URI;
import java.util.List;

import javax.annotation.Nonnull;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.conversion.BlobURIProvider;
import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
import org.osgi.framework.BundleContext;

/**
* Exposed OSGi service implementing the BlobURIProvider.
* This is the service that components outside Oak should
*/
@Component( immediate = true)
@Service(BlobURIProvider.class)
public class BlobURIProviderImpl implements BlobURIProvider {

private Whiteboard whiteboard;

public BlobURIProviderImpl() {}

@Activate
public void activate(BundleContext bundleContext) {
whiteboard = new OsgiWhiteboard(bundleContext);
}

public BlobURIProviderImpl(@Nonnull Whiteboard whiteboard) {
this.whiteboard = whiteboard;
}

@Override
public URI getURI(Blob blob) {
List<BlobURIProvider> converters = WhiteboardUtils.getServices(whiteboard, BlobURIProvider.class);
for (BlobURIProvider converter : converters) {
URI result = converter.getURI(blob);
if (result != null) {
return result;
}
}
return null;
}
}

0 comments on commit 2709c09

Please sign in to comment.