/* * 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 com.sun.jini.logging.Levels; import com.sun.jini.logging.LogManager; import java.io.IOException; import java.net.InetAddress; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.logging.Logger; /** * A stream handler for URLs with the HTTPMD protocol. HTTPMD URLs provide a * way to insure the integrity of data retrieved from an HTTP URL. The HTTPMD * URL includes a message digest for the data to be retrieved from the URL. The * URL input stream insures that the data has the correct message digest when * the end of file for the stream is reached. If the data has the wrong * message digest, a {@link WrongMessageDigestException} is thrown. <p> * * HTTPMD URLs may be used to guarantee the integrity of a downloaded object's * codebase if used when specifying the URLs for the JAR files containing the * object's classes. Because HTTPMD URLs specify a message digest for a single * item, they should not be used for directories of classes. <p> * * HTTPMD URLs have a syntax similar to that of HTTP URLs, but include a * message digest as the last parameter stored in the last segment of the * path. The parameter is introduced by the '<code>;</code>' character, and * includes the name of the message digest algorithm, a '<code>=</code>', the * message digest, and an optional comment introduced by the '<code>,</code>' * character. In addition, a comment by itself may be specified in a relative * HTTPMD URL. Comments are ignored when using <code>equals</code> to compare * HTTPMD URLs. The comment specified in the context URL is ignored when * parsing a relative HTTPMD URL. Adding a comment to an HTTPMD URL is useful * in cases where the URL is required to have a particular suffix, for example * the ".jar" file extension. A comment-only relative HTTPMD URL is useful when * specifying the URL of the containing document from within the contents of * the document, where the message digest cannot be specified because it is not * yet known. <p> * * The message digest algorithm is case-insensitive, and may include ASCII * letters and numbers, as well as the following characters: * * <pre> * - _ . ~ * ' ( ) : @ & + $ , * </pre> <p> * * The value specifies the name of the {@link MessageDigest} algorithm to * use. For the URL syntax to be valid, the value must be the name of a * <code>MessageDigest</code> algorithm as determined by calling * {@link MessageDigest#getInstance(String)}. <p> * * The message digest is represented as a positive hexadecimal integer, using * digits, and the letters '<code>a</code>' through '<code>f</code>', in either * lowercase or uppercase. <p> * * The characters following the '<code>,</code>' comment character may include * ASCII letters and numbers, as well as the following characters: * * <pre> * - _ . ~ * ' ( ) : @ & = + $ , * </pre> <p> * * Here are some examples of HTTPMD URLs: <p> <ul> * * <li> An absolute URL: <br> * <code> * httpmd://www.sun.com/index.html;md5=7be207c7111e459eeea1c9b3d04f1667 * </code> * * <li> A relative URL: <br> * <code>index.html;sha=99f6837808c0a79398bf69d83cfb1b82d20cf0cf,Comment * </code> * * <li> A comment-only relative URL: <br> * <code>,.jar</code> * </ul> * * @author Sun Microsystems, Inc. * @see HttpmdUtil * @since 2.0 * * @com.sun.jini.impl <!-- Implementation Specifics --> * * This implementation of HTTPMD URLs uses the {@link Logger} named * <code>net.jini.url.httpmd</code> to log information at the following logging * levels: <p> * * <table border="1" cellpadding="5" summary="Describes logging performed * by the HTTPMD URL handler at different logging levels"> * * <caption halign="center" valign="top"><b><code> * net.jini.url.httpmd</code></b></caption> * * <tr> <th scope="col"> Level <th scope="col"> Description * * <tr> <td> {@link Levels#FAILED FAILED} <td> URL input stream detects an * incorrect message digest * * </table> <p> * * See the {@link LogManager} class for one way to use the <code>FAILED</code> * logging level in standard logging configuration files. */ public class Handler extends URLStreamHandler { /** Creates a URL stream handler for HTTPMD URLs. */ public Handler() { } /** * Returns the default port for a URL parsed by this handler, which is * <code>80</code>. * * @return the default port for a URL parsed by this handler */ protected int getDefaultPort() { return 80; } /** Creates a HTTP URL connection for an HTTPMD URL. */ protected URLConnection openConnection(URL u) throws IOException { return new HttpmdURLConnection(u); } /** * Parses the string representation of an HTTPMD URL object. * * @throws IllegalArgumentException if the URL is malformed */ protected void parseURL(URL url, String spec, int start, int limit) { /* Check for a relative URL that only specifies a comment */ if (start < limit && spec.charAt(start) == ',') { String query = url.getQuery(); int queryStart = spec.indexOf('?', start); if (queryStart != -1) { query = spec.substring(queryStart + 1, limit); limit = queryStart; } String path = url.getPath() == null ? "" : url.getPath(); /* Remove the comment from the context path */ int param = path.lastIndexOf(';'); if (param != -1) { int comment = path.indexOf(',', param); if (comment != -1) { path = path.substring(0, comment); } } /* Append the new comment */ path += spec.substring(start, limit); setURL(url, url.getProtocol(), url.getHost(), url.getPort(), url.getAuthority(), url.getUserInfo(), path, query, url.getRef()); } else { /* Otherwise, combine spec with the context URL */ super.parseURL(url, spec, start, limit); } /* Check syntax of message digest parameter */ String path = url.getPath() == null ? "" : url.getPath(); int semiIndex = path.lastIndexOf(';'); if (semiIndex < 0) { throw new IllegalArgumentException( "Message digest parameter is missing"); } int equalsIndex = path.indexOf('=', semiIndex); if (equalsIndex < 0) { throw new IllegalArgumentException( "Message digest parameter is missing a '='"); } String algorithm = path.substring(semiIndex + 1, equalsIndex); try { MessageDigest.getInstance(algorithm); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException( "Message digest parameter algorithm is not found: " + algorithm); } String digest = path.substring(equalsIndex + 1); int comment = digest.indexOf(','); if (comment != -1) { for (int i = digest.length(); --i > comment; ) { char c = digest.charAt(i); if (!HttpmdUtil.commentChar(c)) { throw new IllegalArgumentException( "Comment contains illegal character: '" + c + "'"); } } digest = digest.substring(0, comment); } int length = digest.length(); if (length == 0) { throw new IllegalArgumentException( "Message digest parameter digest is empty"); } for (int i = length; --i >= 0; ) { char c = digest.charAt(i); if (Character.digit(c, 16) < 0) { throw new IllegalArgumentException( "Message digest parameter has invalid hex character: " + c); } } } /** * Compares two HTTPMD URLs to see if they refer to the same file. Performs * case-insensitive comparison of the protocols and of the message digest * parameters (ignoring the comment), calls <code>hostsEqual</code> to * compare the hosts, compares the ports, with <code>-1</code> matching the * default HTTP port (<code>80</code>), and performs case-sensitive * comparison on the remaining portions. */ protected boolean sameFile(URL u1, URL u2) { /* Compare the protocols */ if (!((u1.getProtocol() == u2.getProtocol()) || (u1.getProtocol() != null && u1.getProtocol().equalsIgnoreCase(u2.getProtocol())))) { return false; } /* Compare the hosts */ if (!hostsEqual(u1, u2)) { return false; } /* Compare the paths */ String path1 = u1.getPath(); String path2 = u2.getPath(); if (path1 == path2) { /* Paths are OK */ } else if (path1 == null || path2 == null) { return false; } else if (!path1.equals(path2)) { /* * Perform case insensitive matching on the message digest * parameters, ignoring comments. */ int param = path1.lastIndexOf(';'); if (param < 0 || param != path2.lastIndexOf(';')) { return false; } /* Case-sensitive match for non-parameter part */ if (!path1.regionMatches(0, path2, 0, param)) { return false; } /* Ignore comments */ int comment1 = path1.indexOf(',', param + 1); int len = (comment1 != -1) ? comment1 : path1.length(); int comment2 = path2.indexOf(',', param + 1); int len2 = (comment2 != -1) ? comment2 : path2.length(); if (len != len2) { return false; } /* Case-insensitive match for algorithm and digest */ if (!path1.regionMatches(true, param + 1, path2, param + 1, len - param - 1)) { return false; } } /* Compare queries */ if (u1.getQuery() == null ? u2.getQuery() != null : !u1.getQuery().equals(u2.getQuery())) { return false; } /* Compare the ports */ int port1 = (u1.getPort() != -1) ? u1.getPort() : u1.getDefaultPort(); int port2 = (u2.getPort() != -1) ? u2.getPort() : u2.getDefaultPort(); if (port1 != port2) { return false; } return true; } /** * Computes the hash code for the specified URL. This method ignores the * comment portion of the message digest parameter, and ignores the * case of characters in the message digest and algorithm. */ protected int hashCode(URL u) { int h = 0; /* Generate the protocol part */ String protocol = u.getProtocol(); if (protocol != null) { h += protocol.hashCode(); } /* Generate the host part */ InetAddress addr = getHostAddress(u); if (addr != null) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null) { h += host.toLowerCase().hashCode(); } } /* * Generate the path part, ignoring case in the message digest and * algorithm, and ignoring comment. */ String path = u.getPath(); if (path != null) { int param = path.lastIndexOf(';'); if (param == -1) { h += path.hashCode(); } else { h += path.substring(0, param).hashCode(); int comment = path.indexOf(',', param); if (comment != -1) { path = path.substring(0, comment); } h += path.substring(param).toLowerCase().hashCode(); } } /* Generate the query part */ String query = u.getQuery(); if (query != null) { h += query.hashCode(); } /* Generate the port part */ if (u.getPort() == -1) { h += getDefaultPort(); } else { h += u.getPort(); } /* Generate the ref part */ String ref = u.getRef(); if (ref != null) h += ref.hashCode(); return h; } }