package org.springframework.roo.felix;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.url.AbstractURLStreamHandlerService;
import org.osgi.service.url.URLConstants;
import org.osgi.service.url.URLStreamHandlerService;
import org.springframework.roo.felix.pgp.PgpService;
import org.springframework.roo.felix.pgp.SignatureDecision;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.url.stream.UrlInputStreamService;
/**
* Processes <code>httppgp://</code> URLs. Does not handle HTTPS URLs.
* <p>
* This implementation offers two main features:
* <ul>
* <li>It delegates the downloading process to {@link UrlInputStreamService} so
* that an alternate implementation can be added that may offer more advanced
* capabilities or configuration (eg as available from a hosting IDE)</li>
* <li>It downloads an .asc file (computed by the original URL + ".asc") and
* verifies the signature and that the user trusts the signing key (the .asc
* must be a detached armored signature, as produced via
* "gpg --armor --detach-sign file_to_sign.ext")</li> </li>
* </ul>
* <p>
* As such this module simplifies security management and proxy server
* compatibility for Spring Roo.
*
* @author Ben Alex
* @since 1.1
*/
@Component
@Service
public class HttpPgpUrlStreamHandlerServiceImpl extends AbstractURLStreamHandlerService implements
HttpPgpUrlStreamHandlerService {
private static final Logger LOGGER = HandlerUtils
.getLogger(HttpPgpUrlStreamHandlerServiceImpl.class);
@Reference
private PgpService pgpService;
@Reference
private UrlInputStreamService urlInputStreamService;
protected void activate(final ComponentContext context) {
final Hashtable<String, String> dict = new Hashtable<String, String>();
dict.put(URLConstants.URL_HANDLER_PROTOCOL, "httppgp");
context.getBundleContext().registerService(URLStreamHandlerService.class.getName(), this, dict);
}
@Override
public URLConnection openConnection(final URL u) throws IOException {
// Convert httppgp:// URL into a standard http:// URL
final URL resourceUrl = new URL(u.toExternalForm().replace("httppgp", "http"));
// Add .asc to the end of the standard resource URL
final URL ascUrl = new URL(resourceUrl.toExternalForm() + ".asc");
// Start with the ASC file, as if this is for an untrusted key, there's
// no point download the larger resource
final File ascUrlFile = File.createTempFile("roo_asc", null);
ascUrlFile.deleteOnExit();
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(ascUrlFile);
inputStream = urlInputStreamService.openConnection(ascUrl);
IOUtils.copy(inputStream, outputStream);
} catch (final IOException ioe) {
// This is not considered fatal; it is likely the ASC isn't
// available, so we will continue
ascUrlFile.delete();
} finally {
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
}
// Abort if a signature wasn't downloaded (this is a httppgp:// URL
// after all, so it should be available)
Validate
.isTrue(ascUrlFile.exists(),
"Signature verification file is not available at '%s'; continuing",
ascUrl.toExternalForm());
// Decide if this signature file is well-formed and of a key ID that is
// trusted by the user
InputStream resource = null;
InputStream signature = null;
try {
signature = new FileInputStream(ascUrlFile);
final SignatureDecision decision = pgpService.isSignatureAcceptable(signature);
if (!decision.isSignatureAcceptable()) {
LOGGER.log(Level.SEVERE, "Download URL '" + resourceUrl.toExternalForm() + "' failed");
LOGGER.log(Level.SEVERE,
"This resource was signed with PGP key ID '" + decision.getSignatureAsHex()
+ "', which is not currently trusted");
LOGGER
.log(
Level.SEVERE,
"Use 'pgp key view' to view this key, 'pgp trust' to trust it, or 'pgp automatic trust' to trust any keys");
throw new IOException("Download URL '" + resourceUrl.toExternalForm()
+ "' has untrusted PGP signature " + JdkDelegatingLogListener.DO_NOT_LOG);
}
// So far so good. Next we need the actual resource to ensure the
// ASC file really did sign it
final File resourceFile = File.createTempFile("roo_resource", null);
resourceFile.deleteOnExit();
inputStream = urlInputStreamService.openConnection(resourceUrl);
outputStream = new FileOutputStream(resourceFile);
IOUtils.copy(inputStream, outputStream);
resource = new FileInputStream(resourceFile);
signature = new FileInputStream(ascUrlFile);
Validate.isTrue(pgpService.isResourceSignedBySignature(resource, signature),
"PGP signature illegal for URL '%s'", resourceUrl.toExternalForm());
// Excellent it worked! We don't need the ASC file anymore, so get
// rid of it
ascUrlFile.delete();
return resourceFile.toURI().toURL().openConnection();
} finally {
IOUtils.closeQuietly(resource);
IOUtils.closeQuietly(signature);
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
}
}
}