/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.midp.io.j2me.https;
import java.util.Hashtable;
import java.util.Enumeration;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.microedition.io.*;
import javax.microedition.pki.*;
import com.sun.midp.pki.*;
import com.sun.midp.ssl.*;
import com.sun.midp.main.Configuration;
import com.sun.midp.io.*;
import com.sun.midp.io.j2me.http.*;
import com.sun.midp.midlet.*;
import com.sun.midp.publickeystore.WebPublicKeyStore;
import com.sun.midp.security.*;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
import com.sun.midp.util.Properties;
/**
* This class implements the necessary functionality
* for an HTTPS connection. With support for HTTPS tunneling.
* <center><img src="doc-files/https.gif" width=735 height=193
* ALT="https diagram"></center>
* <p>
* Handshake error codes at the beginning of IOException messages:</p>
* <blockquote><p>
* (1) certificate is expired
* </p><p>
* (2) certificate is not yet valid
* </p><p>
* (3) certificate failed signature verification
* </p><p>
* (4) certificate was signed using an unsupported algorithm
* </p><p>
* (5) certificate was issued by an unrecognized certificate authority
* </p><p>
* (6) certificate does not contain the correct site name
* </p><p>
* (7) certificate chain exceeds the length allowed
* </p><p>
* (8) certificate does not contain a signature
* </p><p>
* (9) version 3 certificate has unrecognized critical extensions
* </p><p>
* (10) version 3 certificate has an inappropriate keyUsage or
* extendedKeyUsage extension
* </p><p>
* (11) certificate in the a chain was not issued by the next
* authority in the chain
* </p><p>
* (12) trusted certificate authority's public key is expired
* </p></blockquote>
*/
public class Protocol extends com.sun.midp.io.j2me.http.Protocol
implements HttpsConnection {
/** Common name label. */
private static final String COMMON_NAME_LABEL = "CN=";
/** Common name label length. */
private static final int COMMON_NAME_LABEL_LENGTH =
COMMON_NAME_LABEL.length();
/**
* Inner class to request security token from SecurityInitializer.
* SecurityInitializer should be able to check this inner class name.
*/
static private class SecurityTrusted
implements ImplicitlyTrustedClass {};
/** This class has a different security domain than the MIDlet suite */
private static SecurityToken classSecurityToken =
SecurityInitializer.requestToken(new SecurityTrusted());
/**
* The methods other than openPrim need to know that the
* permission occurred.
*/
private boolean permissionChecked;
/** True if the owner of this connection is trusted. */
private boolean ownerTrusted;
/**
* Parse the common name out of a distinguished name.
*
* @param name distinguished name
*
* @return common name attribute without the label
*/
private static String getCommonName(String name) {
int start;
int end;
if (name == null) {
return null;
}
/* The common name starts with "CN=" label */
start = name.indexOf(COMMON_NAME_LABEL);
if (start < 0) {
return null;
}
start += COMMON_NAME_LABEL_LENGTH;
end = name.indexOf(';', start);
if (end < 0) {
end = name.length();
}
return name.substring(start, end);
}
/**
* Check to see if the site name given by the user matches the site
* name of subject in the certificate. The method supports the wild card
* character for the machine name if a domain name is included after it.
*
* @param siteName site name the user provided
* @param certName site name of the subject from a certificate
*
* @return true if the common name checks out, else false
*/
private static boolean checkSiteName(String siteName, String certName) {
int startOfDomain;
int domainLength;
if (certName == null) {
return false;
}
// try the easy way first, ignoring case
if ((siteName.length() == certName.length()) &&
siteName.regionMatches(true, 0, certName, 0,
certName.length())) {
return true;
}
if (!certName.startsWith("*.")) {
// not a wild card, done
return false;
}
startOfDomain = siteName.indexOf('.');
if (startOfDomain == -1) {
// no domain name
return false;
}
// skip past the '.'
startOfDomain++;
domainLength = siteName.length() - startOfDomain;
if ((certName.length() - 2) != domainLength) {
return false;
}
// compare the just the domain names, ignoring case
if (siteName.regionMatches(true, startOfDomain, certName, 2,
domainLength)) {
return true;
}
return false;
}
/** collection of "Proxy-" headers as name/value pairs */
private Properties proxyHeaders = new Properties();
/** Underlying SSL connection. */
private SSLStreamConnection sslConnection;
/**
* Create a new instance of this class. Override the some of the values
* in our super class.
*/
public Protocol() {
protocol = "https";
default_port = 443; // 443 is the default port for HTTPS
}
/**
* Sets up the state of the connection, but
* does not actually connect to the server until there's something
* to do.
* <p>
* Warning: A subclass that implements this method, not call this
* method and should also implement the disconnect method.
*
* @param name The URL for the connection, without the
* without the protocol part.
* @param mode The access mode, ignored
* @param timeouts A flag to indicate that the called wants
* timeout exceptions, ignored
*
* @return reference to this connection
*
* @exception IllegalArgumentException If a parameter is invalid.
* @exception ConnectionNotFoundException If the connection cannot be
* found.
* @exception IOException If some other kind of I/O error occurs.
*/
public Connection openPrim(String name, int mode, boolean timeouts)
throws IOException, IllegalArgumentException,
ConnectionNotFoundException {
checkForPermission(name);
initStreamConnection(mode);
url = new HttpUrl(protocol, name);
if (url.port == -1) {
url.port = default_port;
}
if (url.host == null) {
throw new IllegalArgumentException("missing host in URL");
}
hostAndPort = url.host + ":" + url.port;
return this;
}
/**
* Check for the required permission.
*
* @param name name of resource to insert into the permission question
*
* @exception IOInterruptedException if another thread interrupts the
* calling thread while this method is waiting to preempt the
* display.
*/
private void checkForPermission(String name)
throws InterruptedIOException {
MIDletStateHandler midletStateHandler;
MIDletSuite midletSuite;
midletStateHandler = MIDletStateHandler.getMidletStateHandler();
midletSuite = midletStateHandler.getMIDletSuite();
if (midletSuite == null) {
throw new IllegalStateException("This class can't be used " +
"before a suite is started.");
}
name = protocol + ":" + name;
try {
midletSuite.checkForPermission(Permissions.HTTPS, name);
ownerTrusted = midletSuite.isTrusted();
permissionChecked = true;
} catch (InterruptedException ie) {
throw new InterruptedIOException(
"Interrupted while trying to ask the user permission");
}
}
/**
* Get the request header value for the named property.
* @param key property name of specific HTTP 1.1 header field
* @return value of the named property, if found, null otherwise.
*/
public String getRequestProperty(String key) {
/* https handles the proxy fields in a different way */
if (key.toLowerCase().startsWith("proxy-")) {
return proxyHeaders.getPropertyIgnoreCase(key);
}
return super.getRequestProperty(key);
}
/**
* Add the named field to the list of request fields.
*
* @param key key for the request header field.
* @param value the value for the request header field.
*/
protected void setRequestField(String key, String value) {
/* https handles the proxy fields in a different way */
if (key.toLowerCase().startsWith("proxy-")) {
proxyHeaders.setPropertyIgnoreCase(key, value);
return;
}
super.setRequestField(key, value);
}
/**
* Connect to the underlying secure socket transport.
* Perform the SSL handshake and then proceeded to the underlying
* HTTP protocol connect semantics.
*
* @return SSL/TCP stream connection
* @exception IOException is thrown if the connection cannot be opened
*/
protected StreamConnection connect() throws IOException {
StreamConnection sc;
String httpsTunnel;
com.sun.midp.io.j2me.socket.Protocol tcpConnection;
OutputStream tcpOutputStream;
InputStream tcpInputStream;
X509Certificate serverCert;
if (!permissionChecked) {
throw new SecurityException();
}
sc = connectionPool.get(classSecurityToken, protocol,
url.host, url.port);
if (sc != null) {
return sc;
}
// Open socket connection
tcpConnection =
new com.sun.midp.io.j2me.socket.Protocol();
// check to see if a protocol is specified for the tunnel
httpsTunnel = Configuration.getProperty("com.sun.midp.io.http.proxy");
if (httpsTunnel != null) {
// Make the connection to the ssl tunnel
tcpConnection.openPrim(classSecurityToken, "//" + httpsTunnel);
// Do not delay request since this delays the response.
tcpConnection.setSocketOption(SocketConnection.DELAY, 0);
tcpOutputStream = tcpConnection.openOutputStream();
tcpInputStream = tcpConnection.openInputStream();
// Do the handshake with the ssl tunnel
try {
doTunnelHandshake(tcpOutputStream, tcpInputStream);
} catch (IOException ioe) {
String temp = ioe.getMessage();
tcpConnection.close();
tcpOutputStream.close();
tcpInputStream.close();
if (temp.indexOf(" 500 ") > -1) {
throw new ConnectionNotFoundException(temp);
}
throw ioe;
}
} else {
tcpConnection.openPrim(classSecurityToken, "//" + hostAndPort);
// Do not delay request since this delays the response.
tcpConnection.setSocketOption(SocketConnection.DELAY, 0);
tcpOutputStream = tcpConnection.openOutputStream();
tcpInputStream = tcpConnection.openInputStream();
}
tcpConnection.close();
try {
// Get the SSLStreamConnection
sslConnection = new SSLStreamConnection(url.host, url.port,
tcpInputStream, tcpOutputStream,
WebPublicKeyStore.getTrustedKeyStore());
} catch (Exception e) {
try {
tcpInputStream.close();
} catch (Throwable t) {
// Ignore, we are processing an exception
}
try {
tcpOutputStream.close();
} catch (Throwable t) {
// Ignore, we are processing an exception
}
if (e instanceof IOException) {
throw (IOException)e;
} else {
throw (RuntimeException)e;
}
}
try {
serverCert = sslConnection.getServerCertificate();
/*
* if the subject alternate name is a DNS name,
* then use that instead of the common name for a
* site name match
*/
if (serverCert.getSubjectAltNameType() ==
X509Certificate.TYPE_DNS_NAME) {
if (!checkSiteName(url.host,
(String)serverCert.getSubjectAltName())) {
throw new CertificateException(
"Subject alternative name did not match site name",
serverCert, CertificateException.SITENAME_MISMATCH);
}
} else {
String cname = getCommonName(serverCert.getSubject());
if (cname == null) {
throw new CertificateException(
"Common name missing from subject name",
serverCert, CertificateException.SITENAME_MISMATCH);
}
if (!checkSiteName(url.host, cname)) {
throw new CertificateException(serverCert,
CertificateException.SITENAME_MISMATCH);
}
}
return sslConnection;
} catch (Exception e) {
try {
sslConnection.close();
} catch (Throwable t) {
// Ignore, we are processing an exception
}
if (e instanceof IOException) {
throw (IOException)e;
} else {
throw (RuntimeException)e;
}
}
}
/**
* disconnect the current connection.
*
* @param connection connection return from {@link #connect()}
* @param inputStream input stream opened from <code>connection</code>
* @param outputStream output stream opened from <code>connection</code>
* @exception IOException if an I/O error occurs while
* the connection is terminated.
*/
protected void disconnect(StreamConnection connection,
InputStream inputStream, OutputStream outputStream)
throws IOException {
try {
try {
inputStream.close();
} finally {
try {
outputStream.close();
} finally {
connection.close();
}
}
} catch (IOException e) {
if (Logging.REPORT_LEVEL <= Logging.WARNING) {
Logging.report(Logging.WARNING, LogChannels.LC_PROTOCOL,
"Exception while closing streams|connection");
}
} catch (NullPointerException e) {
}
}
/**
* Return the security information associated with this connection.
* If the connection is still in <CODE>Setup</CODE> state then
* the connection is initiated to establish the secure connection
* to the server. The method returns when the connection is
* established and the <CODE>Certificate</CODE> supplied by the
* server has been validated.
* The <CODE>SecurityInfo</CODE> is only returned if the
* connection has been successfully made to the server.
*
* @return the security information associated with this open connection.
*
* @exception CertificateException if the <code>Certificate</code>
* supplied by the server cannot be validated.
* The <code>CertificateException</code> will contain
* the information about the error and indicate the certificate in the
* validation chain with the error.
* @exception IOException if an arbitrary connection failure occurs
*/
public SecurityInfo getSecurityInfo() throws IOException {
ensureOpen();
sendRequest();
if (sslConnection == null) {
/*
* This is a persistent connection so the connect method did
* not get called, so the stream connection of HTTP class
* will be a SSL connection. Get the info from that.
*/
StreamConnection sc =
((StreamConnectionElement)getStreamConnection()).
getBaseConnection();
return ((SSLStreamConnection)sc).getSecurityInfo();
}
return sslConnection.getSecurityInfo();
}
}
|