/* * 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.jeri.tcp; import com.sun.jini.action.GetBooleanAction; import com.sun.jini.jeri.internal.runtime.Util; import com.sun.jini.logging.Levels; import com.sun.jini.logging.LogUtil; import java.io.IOException; import java.io.InputStream; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.channels.SocketChannel; import java.security.AccessController; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.SocketFactory; import net.jini.core.constraint.InvocationConstraints; import net.jini.io.UnsupportedConstraintException; import net.jini.jeri.Endpoint; import net.jini.jeri.OutboundRequest; import net.jini.jeri.OutboundRequestIterator; import net.jini.jeri.connection.Connection; import net.jini.jeri.connection.ConnectionEndpoint; import net.jini.jeri.connection.ConnectionManager; import net.jini.jeri.connection.OutboundRequestHandle; import net.jini.security.proxytrust.TrustEquivalence; /** * An implementation of the {@link Endpoint} abstraction that uses TCP * sockets (instances of {@link Socket}) for the underlying * communication mechanism. * * <p><code>TcpEndpoint</code> instances contain a host name and a TCP * port number, as well as an optional {@link SocketFactory} for * customizing the type of <code>Socket</code> to use. The host name * and port number are used as the remote address to connect to when * making socket connections. * * <p><code>TcpEndpoint</code> uses the <a * href="../connection/doc-files/mux.html">Jini extensible remote * invocation (Jini ERI) multiplexing protocol</a> to map outgoing * requests to socket connections. * * <p>A <code>SocketFactory</code> used with a * <code>TcpEndpoint</code> should be serializable and must implement * {@link Object#equals Object.equals} to obey the guidelines that are * specified for <code>equals</code> methods of {@link Endpoint} * instances. * * @author Sun Microsystems, Inc. * @see TcpServerEndpoint * @since 2.0 **/ public final class TcpEndpoint implements Endpoint, TrustEquivalence, Serializable { private static final long serialVersionUID = -2840731722681368933L; /** * weak set of canonical instances; in order to use WeakHashMap, * maps canonical instances to weak references to themselves **/ private static final Map internTable = new WeakHashMap(); /** client transport logger */ private static final Logger logger = Logger.getLogger("net.jini.jeri.tcp.client"); /** whether or not to use NIO-based sockets if possible */ private static final boolean useNIO = // default false ((Boolean) AccessController.doPrivileged(new GetBooleanAction( "com.sun.jini.jeri.tcp.useNIO"))).booleanValue(); /** * The host that this <code>TcpEndpoint</code> connects to. * * @serial **/ private final String host; /** * The TCP port that this <code>TcpEndpoint</code> connects to. * * @serial **/ private final int port; /** * The socket factory that this <code>TcpEndpoint</code> uses to * create <code>Socket</code> objects. * * @serial **/ private final SocketFactory sf; private transient ConnectionManager connectionManager; /** * Returns a <code>TcpEndpoint</code> instance for the given * host name and TCP port number. * * <p>The <code>SocketFactory</code> contained in the returned * <code>TcpEndpoint</code> will be <code>null</code>, indicating * that this endpoint will create <code>Socket</code> objects * directly. * * @param host the host for the endpoint to connect to * * @param port the TCP port on the given host for the endpoint to * connect to * * @return a <code>TcpEndpoint</code> instance * * @throws IllegalArgumentException if the port number is out of * the range <code>1</code> to <code>65535</code> (inclusive) * * @throws NullPointerException if <code>host</code> is * <code>null</code> **/ public static TcpEndpoint getInstance(String host, int port) { return intern(new TcpEndpoint(host, port, null)); } /** * Returns a <code>TcpEndpoint</code> instance for the given host * name and TCP port number that contains the given * <code>SocketFactory</code>. * * <p>If the socket factory argument is <code>null</code>, then * this endpoint will create <code>Socket</code> objects directly. * * @param host the host for the endpoint to connect to * * @param port the TCP port on the given host for the endpoint to * connect to * * @param sf the <code>SocketFactory</code> to use for this * <code>TcpEndpoint</code>, or <code>null</code> * * @return a <code>TcpEndpoint</code> instance * * @throws IllegalArgumentException if the port number is out of * the range <code>1</code> to <code>65535</code> (inclusive) * * @throws NullPointerException if <code>host</code> is * <code>null</code> **/ public static TcpEndpoint getInstance(String host, int port, SocketFactory sf) { return intern(new TcpEndpoint(host, port, sf)); } /** * Returns canonical instance equivalent to given instance. **/ private static TcpEndpoint intern(TcpEndpoint endpoint) { synchronized (internTable) { Reference ref = (WeakReference) internTable.get(endpoint); if (ref != null) { TcpEndpoint canonical = (TcpEndpoint) ref.get(); if (canonical != null) { return canonical; } } endpoint.connectionManager = new ConnectionManager(endpoint.new ConnectionEndpointImpl()); internTable.put(endpoint, new WeakReference(endpoint)); return endpoint; } } /** * Constructs a new instance. **/ private TcpEndpoint(String host, int port, SocketFactory sf) { if (host == null) { throw new NullPointerException(); } if (port < 1 || port > 0xFFFF) { throw new IllegalArgumentException( "port number out of range: " + port); } this.host = host; this.port = port; this.sf = sf; } /* * [This is not a doc comment to prevent its appearance in * TcpEndpoint's serialized form specification.] * * Resolves deserialized instance to equivalent canonical instance. */ private Object readResolve() { return intern(this); } /** * Returns the host that this <code>TcpEndpoint</code> connects to. * * @return the host that this endpoint connects to **/ public String getHost() { return host; } /** * Returns the TCP port that this <code>TcpEndpoint</code> connects to. * * @return the TCP port that this endpoint connects to **/ public int getPort() { return port; } /** * Returns the <code>SocketFactory</code> that this endpoint uses * to create <code>Socket</code> objects. * * @return the socket factory that this endpoint uses to create * sockets, or <code>null</code> if no factory is used **/ public SocketFactory getSocketFactory() { return sf; } /** * {@inheritDoc} * * <p>The returned <code>OutboundRequestIterator</code>'s {@link * OutboundRequestIterator#next next} method behaves as follows: * * <blockquote> * * Initiates an attempt to communicate the request to this remote * endpoint. * * <p>When the implementation of this method needs to create a new * <code>Socket</code>, it will do so by invoking one of the * <code>createSocket</code> methods on the * <code>SocketFactory</code> of this <code>TcpEndpoint</code> * (which produced this iterator) if non-<code>null</code>, or it * will create a <code>Socket</code> directly otherwise. * * <p>When the implementation needs to connect a * <code>Socket</code>, if the host name to connect to (this * <code>TcpEndpoint</code>'s host name) resolves to multiple * addresses (according to {@link InetAddress#getAllByName * InetAddress.getAllByName}), it attempts to connect to the first * resolved address; if that attempt fails with an * <code>IOException</code> or a <code>SecurityException</code>, * it then attempts to connect to the next address; and this * iteration continues as long as there is another resolved * address and the attempt to connect to the previous address * fails with an <code>IOException</code> or a * <code>SecurityException</code>. If the host name resolves to * just one address, the implementation makes one attempt to * connect to that address. If the host name does not resolve to * any addresses (<code>InetAddress.getAllByName</code> would * throw an <code>UnknownHostException</code>), the implementation * still makes an attempt to connect the <code>Socket</code> to * that host name, which could result in an * <code>UnknownHostException</code>. If the final connection * attempt fails with an <code>IOException</code> or a * <code>SecurityException</code>, then if any connection attempt * failed with an <code>IOException</code>, this method throws an * <code>IOException</code>, and otherwise (if all connection * attempts failed with a <code>SecurityException</code>), this * method throws a <code>SecurityException</code>. * * <p>If there is a security manager: * * <ul> * * <li>If a new connection is to be created, the security * manager's {@link SecurityManager#checkConnect(String,int) * checkConnect} method is invoked with this * <code>TcpEndpoint</code>'s host and <code>-1</code> for the * port; if this results in a <code>SecurityException</code>, this * method throws that exception. <code>checkConnect</code> is * also invoked for each connection attempt, with the remote IP * address (or the host name, if it could not be resolved) and * port to connect to; this could result in a * <code>SecurityException</code> for that attempt. (Note that * the implementation may carry out these security checks * indirectly, such as through invocations of * <code>InetAddress.getAllByName</code> or <code>Socket</code>'s * constructors or <code>connect</code> method.) * * <li><p>In order to reuse an existing connection for the * communication, the current security context must have all of * the permissions that would be necessary if the connection were * being created. Specifically, it must be possible to invoke * <code>checkConnect</code> in the current security context with * this <code>TcpEndpoint</code>'s host and <code>-1</code> for * the port without resulting in a <code>SecurityException</code>, * and it also must be possible to invoke * <code>checkConnect</code> with the remote IP address and port * of the <code>Socket</code> without resulting in a * <code>SecurityException</code> (if the remote socket address is * unresolved, its host name is used instead). If no existing * connection satisfies these requirements, then this method must * behave as if there are no existing connections. * * </ul> * * <p>Throws {@link NoSuchElementException} if this iterator does * not support making another attempt to communicate the request * (that is, if <code>hasNext</code> would return * <code>false</code>). * * <p>Throws {@link IOException} if an I/O exception occurs while * performing this operation, such as if a connection attempt * timed out or was refused. * * <p>Throws {@link SecurityException} if there is a security * manager and an invocation of its <code>checkConnect</code> * method fails. * * </blockquote> * * @throws NullPointerException {@inheritDoc} **/ public OutboundRequestIterator newRequest(final InvocationConstraints constraints) { if (constraints == null) { throw new NullPointerException(); } try { Constraints.Distilled distilled = Constraints.distill(constraints, false); return connectionManager.newRequest(new Handle(distilled)); } catch (final UnsupportedConstraintException e) { return new OutboundRequestIterator() { private boolean nextCalled = false; public boolean hasNext() { return !nextCalled; } public OutboundRequest next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } nextCalled = true; e.fillInStackTrace(); // REMIND: is this cool? throw e; } }; } } /** * Returns the hash code value for this <code>TcpEndpoint</code>. * * @return the hash code value for this <code>TcpEndpoint</code> **/ public int hashCode() { return host.hashCode() ^ port ^ (sf != null ? sf.hashCode() : 0); } /** * Compares the specified object with this * <code>TcpEndpoint</code> for equality. * * <p>This method returns <code>true</code> if and only if * * <ul> * * <li>the specified object is also a <code>TcpEndpoint</code>, * * <li>the host and port in the specified object are equal to the * host and port in this object, and * * <li>either this object and the specified object both have no * <code>SocketFactory</code> or the <code>SocketFactory</code> in * the specified object has the same class and is equal to the one * in this object. * * </ul> * * @param obj the object to compare with * * @return <code>true</code> if <code>obj</code> is equivalent to * this object; <code>false</code> otherwise **/ public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof TcpEndpoint)) { return false; } TcpEndpoint other = (TcpEndpoint) obj; return host.equals(other.host) && port == other.port && Util.sameClassAndEquals(sf, other.sf); } /** * Returns <code>true</code> if the specified object (which is not * yet known to be trusted) is equivalent in trust, content, and * function to this known trusted object, and <code>false</code> * otherwise. * * <p>This method returns <code>true</code> if and only if * * <ul> * * <li>the specified object is also a <code>TcpEndpoint</code>, * * <li>the host and port in the specified object are equal to the * host and port in this object, and * * <li>either this object and the specified object both have no * <code>SocketFactory</code> or the <code>SocketFactory</code> in * the specified object has the same class and is equal to the one * in this object. * * </ul> **/ public boolean checkTrustEquivalence(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof TcpEndpoint)) { return false; } TcpEndpoint other = (TcpEndpoint) obj; return host.equals(other.host) && port == other.port && Util.sameClassAndEquals(sf, other.sf); } /** * Returns a string representation of this * <code>TcpEndpoint</code>. * * @return a string representation of this * <code>TcpEndpoint</code> **/ public String toString() { return "TcpEndpoint[" + host + ":" + port + (sf != null ? "," + sf : "") + "]"; } /** * @throws InvalidObjectException if the host name is * <code>null</code> or if the port number is out of the range * <code>1</code> to <code>65535</code> (inclusive) **/ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); if (host == null) { throw new InvalidObjectException("null host"); } if (port < 1 || port > 0xFFFF) { throw new InvalidObjectException( "port number out of range: " + port); } } /** * OutboundRequestHandle implementation. **/ private class Handle implements OutboundRequestHandle { private final Constraints.Distilled distilled; Handle(Constraints.Distilled distilled) { this.distilled = distilled; } TcpEndpoint getTcpEndpoint() { return TcpEndpoint.this; } Constraints.Distilled getDistilledConstraints() { return distilled; } InvocationConstraints getUnfulfilledConstraints() { return distilled.getUnfulfilledConstraints(); } } /** * ConnectionEndpoint implementation. * * Instances of this class should never get exposed to anything * other than our ConnectionManager, which we trust to operate * correctly, so we do not bother to validate request handles and * connections passed in. **/ private class ConnectionEndpointImpl implements ConnectionEndpoint { ConnectionEndpointImpl() { } /** * Invoked by ConnectionManager to create a new connection. **/ public Connection connect(OutboundRequestHandle handle) throws IOException { Handle h = (Handle) handle; Constraints.Distilled distilled = h.getDistilledConstraints(); Socket socket = connectToHost(distilled); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "connected socket {0}", socket); } setSocketOptions(socket); return new ConnectionImpl(socket); } /** * Returns a socket connected to this endpoint's host and * port, according the specified constraints. If the host * name resolves to multiple addresses, attempts to connect to * each of them in order until one succeeds. **/ private Socket connectToHost(Constraints.Distilled distilled) throws IOException { InetAddress[] addresses; try { addresses = InetAddress.getAllByName(host); } catch (UnknownHostException uhe) { try { /* * Creating the InetSocketAddress attempts to * resolve the host again; in J2SE 5.0, there is a * factory method for creating an unresolved * InetSocketAddress directly. */ return connectToSocketAddress( new InetSocketAddress(host, port), distilled); } catch (IOException e) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to unresolved host {0}", new Object[] { host + ":" + port }, e); } throw e; } catch (SecurityException e) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to unresolved host {0}", new Object[] { host + ":" + port }, e); } throw e; } } catch (SecurityException e) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connectToHost", "exception resolving host {0}", new Object[] { host }, e); } throw e; } IOException lastIOException = null; SecurityException lastSecurityException = null; for (int i = 0; i < addresses.length; i++) { SocketAddress socketAddress = new InetSocketAddress(addresses[i], port); try { return connectToSocketAddress(socketAddress, distilled); } catch (IOException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to {0}", new Object[] { socketAddress }, e); } lastIOException = e; if (e instanceof SocketTimeoutException) { break; } } catch (SecurityException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to {0}", new Object[] { socketAddress }, e); } lastSecurityException = e; } } if (lastIOException != null) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to {0}", new Object[] { host + ":" + port }, lastIOException); } throw lastIOException; } assert lastSecurityException != null; if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connectToHost", "exception connecting to {0}", new Object[] { host + ":" + port }, lastSecurityException); } throw lastSecurityException; } /** * Returns a socket connected to the specified address, with a * timeout governed by the specified constraints. **/ private Socket connectToSocketAddress(SocketAddress socketAddress, Constraints.Distilled distilled) throws IOException { int timeout; if (distilled.hasConnectDeadline()) { long now = System.currentTimeMillis(); long deadline = distilled.getConnectDeadline(); if (deadline <= now) { throw new SocketTimeoutException( "deadline past before connect attempt"); } assert now > 0; // if not, we could overflow long delta = deadline - now; // assume that no socket connect will last over 24 days timeout = (delta > Integer.MAX_VALUE ? 0 : (int) delta); } else { timeout = 0; } Socket socket = newSocket(); boolean ok = false; try { socket.connect(socketAddress, timeout); ok = true; return socket; } finally { if (!ok) { try { socket.close(); } catch (IOException e) { } } } } /** * Returns a new unconnected socket, using this endpoint's * socket factory if non-null. **/ private Socket newSocket() throws IOException { Socket socket; if (sf != null) { socket = sf.createSocket(); } else { if (useNIO) { socket = SocketChannel.open().socket(); } else { socket = new Socket(); } } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, (sf == null ? "created socket {0}" : "created socket {0} using factory {1}"), new Object[] { socket, sf }); } return socket; } /** * Invoked by ConnectionManager to reuse an existing * connection. **/ public Connection connect(OutboundRequestHandle handle, Collection active, Collection idle) { if (active == null || idle == null) { throw new NullPointerException(); } /* * The transport level aspects of all constraints * supported by this transport provider are always * satisfied by all open connections, so we don't need to * consider constraints here. */ boolean checkedResolvePermission = false; for (Iterator i = active.iterator(); i.hasNext();) { ConnectionImpl c = (ConnectionImpl) i.next(); if (!checkedResolvePermission) { try { checkResolvePermission(); } catch (SecurityException e) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connect", "exception resolving host {0}", new Object[] { host }, e); } throw e; } checkedResolvePermission = true; } try { c.checkConnectPermission(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "reusing connection {0}", c.getSocket()); } return c; } catch (SecurityException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, ConnectionEndpointImpl.class, "connect", "access to reuse connection {0} denied", new Object[] { c.getSocket() }, e); } } } for (Iterator i = idle.iterator(); i.hasNext();) { ConnectionImpl c = (ConnectionImpl) i.next(); if (!checkedResolvePermission) { try { checkResolvePermission(); } catch (SecurityException e) { if (logger.isLoggable(Levels.FAILED)) { LogUtil.logThrow(logger, Levels.FAILED, ConnectionEndpointImpl.class, "connect", "exception resolving host {0}", new Object[] { host }, e); } throw e; } checkedResolvePermission = true; } try { c.checkConnectPermission(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "reusing connection {0}", c.getSocket()); } return c; } catch (SecurityException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, ConnectionEndpointImpl.class, "connect", "access to reuse connection {0} denied", new Object[] { c.getSocket() }, e); } } } return null; } private void checkResolvePermission() { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkConnect(host, -1); } } } /** * Connection implementation. * * Instances of this class should never get exposed to anything * other than our ConnectionManager, which we trust to operate * correctly, so we do not bother to validate request handles * passed in. **/ private static class ConnectionImpl implements Connection { private final Socket socket; ConnectionImpl(Socket socket) { this.socket = socket; } Socket getSocket() { return socket; } public InputStream getInputStream() throws IOException { return socket.getInputStream(); } public OutputStream getOutputStream() throws IOException { return socket.getOutputStream(); } public SocketChannel getChannel() { return socket.getChannel(); } public void populateContext(OutboundRequestHandle handle, Collection context) { if (context == null) { throw new NullPointerException(); } } public InvocationConstraints getUnfulfilledConstraints(OutboundRequestHandle handle) { Handle h = (Handle) handle; return h.getUnfulfilledConstraints(); } public void writeRequestData(OutboundRequestHandle handle, OutputStream out) { if (out == null) { throw new NullPointerException(); } } public IOException readResponseData(OutboundRequestHandle handle, InputStream in) { if (in == null) { throw new NullPointerException(); } return null; } public void close() { try { socket.close(); } catch (Exception e) { } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "closed socket {0}", socket); } } void checkConnectPermission() { SecurityManager sm = System.getSecurityManager(); if (sm != null) { InetSocketAddress address = (InetSocketAddress) socket.getRemoteSocketAddress(); if (address.isUnresolved()) { sm.checkConnect(address.getHostName(), socket.getPort()); } else { sm.checkConnect(address.getAddress().getHostAddress(), socket.getPort()); } } } } /** * Attempts to set desired socket options for a connected socket * (TCP_NODELAY and SO_KEEPALIVE); ignores SocketException. **/ private static void setSocketOptions(Socket socket) { try { socket.setTcpNoDelay(true); } catch (SocketException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, TcpEndpoint.class, "setSocketOptions", "exception setting TCP_NODELAY on socket {0}", new Object[] { socket }, e); } } try { socket.setKeepAlive(true); } catch (SocketException e) { if (logger.isLoggable(Levels.HANDLED)) { LogUtil.logThrow(logger, Levels.HANDLED, TcpEndpoint.class, "setSocketOptions", "exception setting SO_KEEPALIVE on socket {0}", new Object[] { socket }, e); } } } }