/*
* 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 net.jini.url.httpmd;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.StringTokenizer;
import net.jini.security.Security;
/**
* Provides utility methods for creating and using HTTPMD URLs.
*
* @author Sun Microsystems, Inc.
* @since 2.0
*/
public class HttpmdUtil {
/**
* A URL handler to use for HTTPMD URLs so we don't require the protocol
* handler to be installed.
*/
private static final Handler handler = new Handler();
/** Not instantiable. */
private HttpmdUtil() { }
/**
* Computes the message digest of data specified by a URL.
*
* @param url the URL of the data
* @param algorithm the message digest algorithm to use
* @return the message digest, as a <code>String</code> in hexadecimal
* format
* @throws IOException if an I/O exception occurs while reading data from
* the URL
* @throws NoSuchAlgorithmException if no provider is found for the message
* digest algorithm
* @throws NullPointerException if either argument is <code>null</code>
*/
public static String computeDigest(URL url, String algorithm)
throws IOException, NoSuchAlgorithmException
{
return computeDigest(url.openStream(), algorithm);
}
/** Computes the message digest for an input stream. */
private static String computeDigest(InputStream in, String algorithm)
throws IOException, NoSuchAlgorithmException
{
try {
if (!(in instanceof BufferedInputStream)) {
in = new BufferedInputStream(in, 2048);
}
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] buf = new byte[2048];
while (true) {
int n = in.read(buf);
if (n < 0) {
break;
}
md.update(buf, 0, n);
}
return digestString(md.digest());
} finally {
try {
in.close();
} catch (IOException e) {
}
}
}
/**
* Computes the message digests for a codebase with HTTPMD URLs.
* <p>
* Do not use a directory on a remote filesystem, or a directory URL, if
* the underlying network access protocol does not provide adequate data
* integrity or authentication of the remote host.
*
* @param sourceDirectory the filename or URL of the directory containing
* the source files corresponding to the URLs in
* <code>codebase</code>
* @param codebase a space-separated list of HTTPMD URLs. The digest
* values specified in the URLs will be ignored. The path portion of
* the URLs, without the message digest parameters, will be used to
* specify the source files, relative to
* <code>sourceDirectory</code>, to use for computing message
* digests.
* @return the codebase updated to include computed digests
* @throws IllegalArgumentException if any of the URLs in
* <code>codebase</code> fail to specify the HTTPMD protocol
* @throws IOException if an I/O exception occurs while reading data from
* the source files
* @throws MalformedURLException if any of the URLs in
* <code>codebase</code> have incorrect syntax
* @throws NullPointerException if either argument is <code>null</code>
*/
public static String computeDigestCodebase(String sourceDirectory,
String codebase)
throws IOException, MalformedURLException, NullPointerException
{
boolean isURL;
try {
new URL(sourceDirectory);
isURL = true;
} catch (MalformedURLException e) {
isURL = false;
}
if (sourceDirectory.endsWith(isURL ? "/" : File.separator)) {
sourceDirectory =
sourceDirectory.substring(0, sourceDirectory.length() - 1);
}
StringTokenizer specs = new StringTokenizer(codebase);
StringBuffer sb = new StringBuffer();
boolean first = true;
while (specs.hasMoreTokens()) {
final String spec = specs.nextToken();
if (!"httpmd:".regionMatches(true, 0, spec, 0, 7)) {
throw new IllegalArgumentException(
"Codebase URL does not specify HTTPMD protocol: " + spec);
}
/*
* Use doPrivileged so caller doesn't need
* java.net.NetPermission("specifyStreamHandler") to specify the
* URL stream handler.
*/
URL url;
try {
url = (URL) Security.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws MalformedURLException {
return new URL(null, spec, handler);
}
});
} catch (PrivilegedActionException e) {
throw (MalformedURLException) e.getCause();
}
String path = url.getPath();
int paramIndex = path.lastIndexOf(';');
int equalsIndex = path.indexOf('=', paramIndex);
int commentIndex = path.indexOf(',', equalsIndex);
String algorithm = path.substring(paramIndex + 1, equalsIndex);
URI relSourceURI;
try {
relSourceURI = new URI(
"file:" + (path.startsWith("/") ? "" : "/") +
path.substring(0, path.indexOf(';')));
} catch (URISyntaxException e) {
throw new MalformedURLException(
"Problem with codebase URL " + spec + ": " +
e.getMessage());
}
InputStream in;
if (isURL) {
String relSource = relSourceURI.getRawPath();
in = new URL(sourceDirectory + relSource).openStream();
} else {
String relSource = relSourceURI.getPath();
if ('/' != File.separatorChar) {
relSource = relSource.replace('/', File.separatorChar);
}
in = new FileInputStream(sourceDirectory + relSource);
}
String digest;
try {
digest = computeDigest(in, algorithm);
} catch (NoSuchAlgorithmException e) {
/* Shouldn't happen -- URL constructor should find this */
throw new RuntimeException("Shouldn't happen: " + e);
}
URL result = new URL(
url,
path.substring(0, equalsIndex + 1) + digest +
(commentIndex < 0 ? "" : path.substring(commentIndex)) +
(url.getQuery() == null ? "" : '?' + url.getQuery()) +
(url.getRef() == null ? "" : '#' + url.getRef()));
if (!first) {
sb.append(' ');
} else {
first = false;
}
sb.append(result);
}
return sb.toString();
}
/** Converts a message digest to a String in hexadecimal format. */
static String digestString(byte[] digest) {
StringBuffer sb = new StringBuffer(digest.length * 2);
for (int i = 0; i < digest.length; i++) {
byte b = digest[i];
sb.append(Character.forDigit((b >> 4) & 0xf, 16));
sb.append(Character.forDigit(b & 0xf, 16));
}
return sb.toString();
}
/** Converts a String in hexadecimal format to a message digest. */
static byte[] stringDigest(String s) throws NumberFormatException {
byte[] result = new byte[(s.length() + 1) / 2];
int rpos = result.length;
int last = -1;
for (int spos = s.length(); --spos >= 0; ) {
int digit = Character.digit(s.charAt(spos), 16);
if (digit < 0) {
throw new NumberFormatException(
"Illegal hex digit: '" + s.charAt(spos) + "'");
}
if (last < 0) {
last = digit;
} else {
result[--rpos] = (byte) (last + (digit << 4));
last = -1;
}
}
if (last >= 0) {
result[--rpos] = (byte) last;
}
return result;
}
/**
* Returns true if the character is permitted in an HTTPMD comment. Legal
* comment characters are ASCII letters and numbers, plus '-', '_', '.',
* '~', '*', ''', '(', ')', ':', '@', '&', '=', '+', '$', and ','.
*/
static boolean commentChar(char c) {
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| ("-_.~*'():@&=+$,".indexOf(c) >= 0);
}
}