/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4che3.net; import java.io.Closeable; import java.io.IOException; import java.io.Serializable; import java.net.*; import java.security.GeneralSecurityException; import java.util.*; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.dcm4che3.conf.core.api.ConfigurableClass; import org.dcm4che3.conf.core.api.ConfigurableProperty; import org.dcm4che3.conf.core.api.ConfigurableProperty.ConfigurablePropertyType; import org.dcm4che3.conf.core.api.ConfigurableProperty.Tag; import org.dcm4che3.conf.core.api.LDAP; import org.dcm4che3.net.proxy.ProxyManager; import org.dcm4che3.net.proxy.ProxyService; import org.dcm4che3.util.SafeClose; import org.dcm4che3.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A DICOM Part 15, Annex H compliant class, <code>NetworkConnection</code> * encapsulates the properties associated with a connection to a TCP/IP network. * <p> * The <i>network connection</i> describes one TCP port on one network device. * This can be used for a TCP connection over which a DICOM association can be * negotiated with one or more Network AEs. It specifies 8 the hostname and TCP * port number. A network connection may support multiple Network AEs. The * Network AE selection takes place during association negotiation based on the * called and calling AE-titles. * * @author Gunter Zeilinger <gunterze@gmail.com> */ @LDAP(objectClasses = {"dicomNetworkConnection", "dcmNetworkConnection"}) @ConfigurableClass(referable = true) public class Connection implements Serializable { private static final long serialVersionUID = -7814748788035232055L; public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public enum Protocol { DICOM, HL7, SYSLOG_TLS, SYSLOG_UDP; public boolean isTCP() { return this != SYSLOG_UDP; } public boolean isSyslog() { return this == SYSLOG_TLS || this == SYSLOG_UDP; } } public static final Logger LOG = LoggerFactory.getLogger(Connection.class); public static final String NO_TIMEOUT_STR = "0"; public static final int NO_TIMEOUT = Integer.valueOf(NO_TIMEOUT_STR); public static final int SYNCHRONOUS_MODE = 1; public static final int NOT_LISTENING = -1; public static final int DEF_BACKLOG = 50; public static final int DEF_SOCKETDELAY = 50; public static final String DEF_BUFFERSIZE_STR = "0"; public static final int DEF_BUFFERSIZE = Integer.valueOf(DEF_BUFFERSIZE_STR); public static final int DEF_MAX_PDU_LENGTH = 16378; // to fit into SunJSSE TLS Application Data Length 16408 public static final String TLS_RSA_WITH_NULL_SHA = "SSL_RSA_WITH_NULL_SHA"; public static final String TLS_RSA_WITH_3DES_EDE_CBC_SHA = "SSL_RSA_WITH_3DES_EDE_CBC_SHA"; public static final String TLS_RSA_WITH_AES_128_CBC_SHA = "TLS_RSA_WITH_AES_128_CBC_SHA"; private static final String[] DEFAULT_TLS_PROTOCOLS = { "TLSv1.2", "TLSv1.1", "TLSv1" }; private Device device; @ConfigurableProperty(name = "cn", label = "Name", tags = Tag.PRIMARY) private String commonName; @ConfigurableProperty(name = "dicomHostname", label = "Hostname", tags = Tag.PRIMARY) private String hostname; @ConfigurableProperty(type = ConfigurablePropertyType.OptimisticLockingHash) private String olockHash; @ConfigurableProperty(type = ConfigurablePropertyType.UUID) private String uuid = UUID.randomUUID().toString(); @ConfigurableProperty(name = "dcmBindAddress") private String bindAddress; @ConfigurableProperty(name = "dcmClientBindAddress") private String clientBindAddress; @ConfigurableProperty(name = "dcmHTTPProxy") private String httpProxy; @ConfigurableProperty( name = "dicomPort", defaultValue = "-1", label = "Port", tags = Tag.PRIMARY) private int port = NOT_LISTENING; @ConfigurableProperty(name = "dcmTCPBacklog", defaultValue = "50") private int backlog = DEF_BACKLOG; @ConfigurableProperty(name = "dcmTCPConnectTimeout", defaultValue = NO_TIMEOUT_STR) private int connectTimeout; @ConfigurableProperty(name = "dcmAARQTimeout", defaultValue = NO_TIMEOUT_STR) private int requestTimeout; @ConfigurableProperty(name = "dcmAAACTimeout", defaultValue = NO_TIMEOUT_STR) private int acceptTimeout; @ConfigurableProperty(name = "dcmARRPTimeout", defaultValue = NO_TIMEOUT_STR) private int releaseTimeout; @ConfigurableProperty(name = "dcmResponseTimeout", defaultValue = NO_TIMEOUT_STR) private int responseTimeout; @ConfigurableProperty(name = "dcmRetrieveTimeout", defaultValue = NO_TIMEOUT_STR) private int retrieveTimeout; @ConfigurableProperty(name = "dcmIdleTimeout", defaultValue = NO_TIMEOUT_STR) private int idleTimeout; @ConfigurableProperty(name = "dcmTCPCloseDelay", defaultValue = "50") private int socketCloseDelay = DEF_SOCKETDELAY; @ConfigurableProperty(name = "dcmTCPSendBufferSize", defaultValue = DEF_BUFFERSIZE_STR) private int sendBufferSize; @ConfigurableProperty(name = "dcmTCPReceiveBufferSize", defaultValue = DEF_BUFFERSIZE_STR) private int receiveBufferSize; @ConfigurableProperty(name = "dcmSendPDULength", defaultValue = "16378") private int sendPDULength = DEF_MAX_PDU_LENGTH; @ConfigurableProperty(name = "dcmReceivePDULength", defaultValue = "16378") private int receivePDULength = DEF_MAX_PDU_LENGTH; @ConfigurableProperty(name = "dcmMaxOpsPerformed", defaultValue = "1") private int maxOpsPerformed = SYNCHRONOUS_MODE; @ConfigurableProperty(name = "dcmMaxOpsInvoked", defaultValue = "1") private int maxOpsInvoked = SYNCHRONOUS_MODE; @ConfigurableProperty(name = "dcmPackPDV", defaultValue = "true") private boolean packPDV = true; @ConfigurableProperty(name = "dcmTCPNoDelay", defaultValue = "true") private boolean tcpNoDelay = true; @ConfigurableProperty(name = "dcmTLSNeedClientAuth", defaultValue = "true") private boolean tlsNeedClientAuth = true; @ConfigurableProperty(name = "dicomTLSCipherSuite") private String[] tlsCipherSuites = {}; @ConfigurableProperty(name = "dcmHTTPProxyProviderName", defaultValue = ProxyService.DEFAULT_PROVIDER_NAME) private String httpProxyProviderName = ProxyService.DEFAULT_PROVIDER_NAME; @ConfigurableProperty(name = "dcmHTTPProxyProviderVersion", defaultValue = ProxyService.DEFAULT_VERSION) private String httpProxyProviderVersion = ProxyService.DEFAULT_VERSION; @ConfigurableProperty(name = "dcmTLSProtocol") private String[] tlsProtocols = {}; @ConfigurableProperty(name = "dcmBlacklistedHostname") private String[] blacklist = {}; @ConfigurableProperty(name = "dicomInstalled") private Boolean connectionInstalled; @ConfigurableProperty(name = "connectionExtensions", isExtensionsProperty = true) private Map<Class<? extends ConnectionExtension>, ConnectionExtension> extensions = new HashMap<Class<? extends ConnectionExtension>, ConnectionExtension>(); @ConfigurableProperty( name = "dcmProtocol", defaultValue = "DICOM", label = "Protocol", tags = Tag.PRIMARY ) private Protocol protocol = Protocol.DICOM; private static final EnumMap<Protocol, TCPProtocolHandler> tcpHandlers = new EnumMap<Protocol, TCPProtocolHandler>(Protocol.class); private static final EnumMap<Protocol, UDPProtocolHandler> udpHandlers = new EnumMap<Protocol, UDPProtocolHandler>(Protocol.class); private transient List<InetAddress> blacklistAddrs; private transient InetAddress hostAddr; private transient InetAddress bindAddr; private transient InetAddress clientBindAddr; private transient volatile Listener listener; private transient boolean rebindNeeded; static { registerTCPProtocolHandler(Protocol.DICOM, DicomProtocolHandler.INSTANCE); } public Connection() { } public Connection(String commonName, String hostname) { this(commonName, hostname, NOT_LISTENING); } public Connection(String commonName, String hostname, int port) { this.commonName = commonName; this.hostname = hostname; this.port = port; } /** * * @param commonName * @param hostname * @param port * @param timeout value in seconds to assign to all timeouts */ public Connection(String commonName, String hostname, int port, int timeout) { this(commonName, hostname, port); setConnectTimeout(timeout); setRequestTimeout(timeout); setAcceptTimeout(timeout); setReleaseTimeout(timeout); setResponseTimeout(timeout); setRetrieveTimeout(timeout); setIdleTimeout(timeout); } public static TCPProtocolHandler registerTCPProtocolHandler( Protocol protocol, TCPProtocolHandler handler) { return tcpHandlers.put(protocol, handler); } public static TCPProtocolHandler unregisterTCPProtocolHandler( Protocol protocol) { return tcpHandlers.remove(protocol); } public static UDPProtocolHandler registerUDPProtocolHandler( Protocol protocol, UDPProtocolHandler handler) { return udpHandlers.put(protocol, handler); } public static UDPProtocolHandler unregisterUDPProtocolHandler( Protocol protocol) { return udpHandlers.remove(protocol); } /** * Get the <code>Device</code> object that this Network Connection belongs * to. * * @return Device */ public Device getDevice() { return device; } /** * Set the <code>Device</code> object that this Network Connection belongs * to. * * @param device The owning <code>Device</code> object. */ public void setDevice(Device device) { if (device != null && this.device != null) throw new IllegalStateException("already owned by " + device); this.device = device; } /** * This is the DNS name for this particular connection. This is used to * obtain the current IP address for connections. Hostname must be * sufficiently qualified to be unambiguous for any client DNS user. * * @return A String containing the host name. */ public final String getHostname() { return hostname; } public String getHttpProxyProviderName() { return httpProxyProviderName; } public String getHttpProxyProviderVersion() { return httpProxyProviderVersion; } /** * This is the DNS name for this particular connection. This is used to * obtain the current IP address for connections. Hostname must be * sufficiently qualified to be unambiguous for any client DNS user. * * @param hostname A String containing the host name. */ public final void setHostname(String hostname) { if (hostname != null ? hostname.equals(this.hostname) : this.hostname == null) return; this.hostname = hostname; needRebind(); } /** * Bind address of listening socket or {@code null}. If {@code null}, bind * listening socket to {@link #getHostname()}. This is the default. * <p> * The bind address can also include system properties with the * <em>${system.property}</em> syntax, that need to be resolved. * * @return Bind address of the connection or {@code null} */ public final String getBindAddress() { return bindAddress; } /** * Bind address of listening socket or {@code null}. If {@code null}, bind * listening socket to {@link #getHostname()}. * <p> * The bind address can also include system properties with the * <em>${system.property}</em> syntax, that will be resolved. * * @param bindAddress * Bind address of listening socket or {@code null} */ public final void setBindAddress(String bindAddress) { if (bindAddress != null ? bindAddress.equals(this.bindAddress) : this.bindAddress == null) return; this.bindAddress = bindAddress; this.bindAddr = null; needRebind(); } /** * Bind address of outgoing connections, {@code "0.0.0.0"} or {@code null}. * If {@code "0.0.0.0"} the system pick up any local ip for outgoing * connections. If {@code null}, bind outgoing connections to * {@link #getHostname()}. This is the default. * * @return Bind address of outgoing connection, {@code 0.0.0.0} or * {@code null} */ public String getClientBindAddress() { return clientBindAddress; } public ProxyManager getProxyManager() { return ProxyService.getInstance().getProxyManager(this.httpProxyProviderName,this.httpProxyProviderVersion); } /** * Bind address of outgoing connections, {@code "0.0.0.0"} or {@code null}. * If {@code "0.0.0.0"} the system pick up any local ip for outgoing * connections. If {@code null}, bind outgoing connections to * {@link #getHostname()}. * * @param bindAddress Bind address of outgoing connection or {@code null} */ public void setClientBindAddress(String bindAddress) { if (bindAddress != null ? bindAddress.equals(this.clientBindAddress) : this.clientBindAddress == null) return; this.clientBindAddress = bindAddress; this.clientBindAddr = null; } public Protocol getProtocol() { return protocol; } public void setProtocol(Protocol protocol) { if (protocol == null) throw new NullPointerException(); if (this.protocol == protocol) return; this.protocol = protocol; needRebind(); } boolean isRebindNeeded() { return rebindNeeded; } void needRebind() { this.rebindNeeded = true; } /** * An arbitrary name for the Network Connections object. Can be a meaningful * name or any unique sequence of characters. * * @return A String containing the name. */ public final String getCommonName() { return commonName; } /** * An arbitrary name for the Network Connections object. Can be a meaningful * name or any unique sequence of characters. * * @param name A String containing the name. */ public final void setCommonName(String name) { this.commonName = name; } /** * The TCP port that the AE is listening on or <code>-1</code> for a * network connection that only initiates associations. * * @return An int containing the port number or <code>-1</code>. */ public final int getPort() { return port; } /** * The TCP port that the AE is listening on or <code>0</code> for a * network connection that only initiates associations. * <p> * A valid port value is between 0 and 65535. * * @param port The port number or <code>-1</code>. */ public final void setPort(int port) { if (this.port == port) return; if ((port <= 0 || port > 0xFFFF) && port != NOT_LISTENING) throw new IllegalArgumentException("port out of range:" + port); this.port = port; needRebind(); } public Map<Class<? extends ConnectionExtension>, ConnectionExtension> getExtensions() { return extensions; } public void setExtensions(Map<Class<? extends ConnectionExtension>, ConnectionExtension> extensions) { this.extensions = extensions; } public <T> T getExtension(Class<T> clazz) { return (T) extensions.get(clazz); } public void addExtension(ConnectionExtension connectionExtension) { connectionExtension.setConnection(this); extensions.put(connectionExtension.getClass(), connectionExtension); } public void setHttpProxyProviderName(String httpProxyProviderName) { this.httpProxyProviderName = httpProxyProviderName; } public void setHttpProxyProviderVersion(String httpProxyProviderVersion) { this.httpProxyProviderVersion = httpProxyProviderVersion; } private void reconfigureExtensions(Connection from) { for (Iterator<Class<? extends ConnectionExtension>> it = extensions.keySet().iterator(); it.hasNext(); ) { if (!from.extensions.containsKey(it.next())) it.remove(); } for (ConnectionExtension src : from.extensions.values()) { Class<? extends ConnectionExtension> clazz = src.getClass(); ConnectionExtension ext = extensions.get(clazz); if (ext == null) try { addExtension(ext = clazz.newInstance()); } catch (Exception e) { throw new RuntimeException( "Failed to instantiate " + clazz.getName(), e); } ext.reconfigure(src); } } public final String getHttpProxy() { return httpProxy; } public final void setHttpProxy(String proxy) { this.httpProxy = proxy; } public final boolean useHttpProxy() { return httpProxy != null; } public final boolean isServer() { return port > 0; } public final int getBacklog() { return backlog; } public final void setBacklog(int backlog) { if (this.backlog == backlog) return; if (backlog < 1) throw new IllegalArgumentException("backlog: " + backlog); this.backlog = backlog; needRebind(); } public final int getConnectTimeout() { return connectTimeout; } public final void setConnectTimeout(int timeout) { if (timeout < 0) throw new IllegalArgumentException("timeout: " + timeout); this.connectTimeout = timeout; } /** * Timeout in ms for receiving A-ASSOCIATE-RQ, 5000 by default * * @param An int value containing the milliseconds. */ public final int getRequestTimeout() { return requestTimeout; } /** * Timeout in ms for receiving A-ASSOCIATE-RQ, 5000 by default * * @param timeout An int value containing the milliseconds. */ public final void setRequestTimeout(int timeout) { if (timeout < 0) throw new IllegalArgumentException("timeout: " + timeout); this.requestTimeout = timeout; } public final int getAcceptTimeout() { return acceptTimeout; } public final void setAcceptTimeout(int timeout) { if (timeout < 0) throw new IllegalArgumentException("timeout: " + timeout); this.acceptTimeout = timeout; } /** * Timeout in ms for receiving A-RELEASE-RP, 5000 by default. * * @return An int value containing the milliseconds. */ public final int getReleaseTimeout() { return releaseTimeout; } /** * Timeout in ms for receiving A-RELEASE-RP, 5000 by default. * * @param timeout An int value containing the milliseconds. */ public final void setReleaseTimeout(int timeout) { if (timeout < 0) throw new IllegalArgumentException("timeout: " + timeout); this.releaseTimeout = timeout; } /** * Delay in ms for Socket close after sending A-ABORT, 50ms by default. * * @return An int value containing the milliseconds. */ public final int getSocketCloseDelay() { return socketCloseDelay; } /** * Delay in ms for Socket close after sending A-ABORT, 50ms by default. * * @param delay An int value containing the milliseconds. */ public final void setSocketCloseDelay(int delay) { if (delay < 0) throw new IllegalArgumentException("delay: " + delay); this.socketCloseDelay = delay; } public final void setResponseTimeout(int timeout) { this.responseTimeout = timeout; } public final int getResponseTimeout() { return responseTimeout; } public final int getRetrieveTimeout() { return retrieveTimeout; } public final void setRetrieveTimeout(int timeout) { this.retrieveTimeout = timeout; } public final int getIdleTimeout() { return idleTimeout; } public final void setIdleTimeout(int idleTimeout) { this.idleTimeout = idleTimeout; } /** * The TLS CipherSuites that are supported on this particular connection. * TLS CipherSuites shall be described using an RFC-2246 string * representation (e.g. 'SSL_RSA_WITH_3DES_EDE_CBC_SHA') * * @return A String array containing the supported cipher suites */ public String[] getTlsCipherSuites() { return tlsCipherSuites; } /** * The TLS CipherSuites that are supported on this particular connection. * TLS CipherSuites shall be described using an RFC-2246 string * representation (e.g. 'SSL_RSA_WITH_3DES_EDE_CBC_SHA') * * @param tlsCipherSuite A String array containing the supported cipher suites */ public void setTlsCipherSuites(String... tlsCipherSuites) { if (Arrays.equals(this.tlsCipherSuites, tlsCipherSuites)) return; this.tlsCipherSuites = tlsCipherSuites; needRebind(); } public final boolean isTls() { return tlsCipherSuites.length > 0; } public final String[] tlsProtocols() { return tlsProtocols.length != 0 ? tlsProtocols : DEFAULT_TLS_PROTOCOLS; } public final String[] getTlsProtocols() { return tlsProtocols; } public final void setTlsProtocols(String... tlsProtocols) { if (Arrays.equals(this.tlsProtocols, tlsProtocols)) return; this.tlsProtocols = tlsProtocols; needRebind(); } public final boolean isTlsNeedClientAuth() { return tlsNeedClientAuth; } public final void setTlsNeedClientAuth(boolean tlsNeedClientAuth) { if (this.tlsNeedClientAuth == tlsNeedClientAuth) return; this.tlsNeedClientAuth = tlsNeedClientAuth; needRebind(); } /** * Get the SO_RCVBUF socket value in KB. * * @return An int value containing the buffer size in KB. */ public final int getReceiveBufferSize() { return receiveBufferSize; } /** * Set the SO_RCVBUF socket option to specified value in KB. * * @param bufferSize An int value containing the buffer size in KB. */ public final void setReceiveBufferSize(int size) { if (size < 0) throw new IllegalArgumentException("size: " + size); this.receiveBufferSize = size; } /** * Get the SO_SNDBUF socket option value in KB, * * @return An int value containing the buffer size in KB. */ public final int getSendBufferSize() { return sendBufferSize; } /** * Set the SO_SNDBUF socket option to specified value in KB, * * @param bufferSize An int value containing the buffer size in KB. */ public final void setSendBufferSize(int size) { if (size < 0) throw new IllegalArgumentException("size: " + size); this.sendBufferSize = size; } public final int getSendPDULength() { return sendPDULength; } public final void setSendPDULength(int sendPDULength) { this.sendPDULength = sendPDULength; } public final int getReceivePDULength() { return receivePDULength; } public final void setReceivePDULength(int receivePDULength) { this.receivePDULength = receivePDULength; } public final int getMaxOpsPerformed() { return maxOpsPerformed; } public final void setMaxOpsPerformed(int maxOpsPerformed) { this.maxOpsPerformed = maxOpsPerformed; } public final int getMaxOpsInvoked() { return maxOpsInvoked; } public final void setMaxOpsInvoked(int maxOpsInvoked) { this.maxOpsInvoked = maxOpsInvoked; } public final boolean isPackPDV() { return packPDV; } public final void setPackPDV(boolean packPDV) { this.packPDV = packPDV; } /** * Determine if this network connection is using Nagle's algorithm as part * of its network communication. * * @return boolean True if TCP no delay (disable Nagle's algorithm) is used. */ public final boolean isTcpNoDelay() { return tcpNoDelay; } /** * Set whether or not this network connection should use Nagle's algorithm * as part of its network communication. * * @param tcpNoDelay boolean True if TCP no delay (disable Nagle's algorithm) * should be used. */ public final void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; } /** * True if the Network Connection is installed on the network. If not * present, information about the installed status of the Network Connection * is inherited from the device. * * @return boolean True if the NetworkConnection is installed on the * network. */ public boolean isInstalled() { return device != null && device.isInstalled() && (connectionInstalled == null || connectionInstalled.booleanValue()); } public Boolean getConnectionInstalled() { return connectionInstalled; } /** * True if the Network Connection is installed on the network. If not * present, information about the installed status of the Network Connection * is inherited from the device. * * @param installed True if the NetworkConnection is installed on the network. * @throws GeneralSecurityException */ public void setConnectionInstalled(Boolean installed) { if (this.connectionInstalled == installed) return; boolean prev = isInstalled(); this.connectionInstalled = installed; if (isInstalled() != prev) needRebind(); } synchronized void rebind() throws IOException, GeneralSecurityException { unbind(); bind(); } /** * Get a list of IP addresses from which we should ignore connections. * Useful in an environment that utilizes a load balancer. In the case of a * TCP ping from a load balancing switch, we don't want to spin off a new * thread and try to negotiate an association. * * @return Returns the list of IP addresses which should be ignored. */ public final String[] getBlacklist() { return blacklist; } /** * Set a list of IP addresses from which we should ignore connections. * Useful in an environment that utilizes a load balancer. In the case of a * TCP ping from a load balancing switch, we don't want to spin off a new * thread and try to negotiate an association. * * @param blacklist the list of IP addresses which should be ignored. */ public final void setBlacklist(String[] blacklist) { this.blacklist = blacklist; this.blacklistAddrs = null; } @Override public String toString() { return promptTo(new StringBuilder(), "").toString(); } public StringBuilder promptTo(StringBuilder sb, String indent) { String indent2 = indent + " "; StringUtils.appendLine(sb, indent, "Connection[cn: ", commonName); StringUtils.appendLine(sb, indent2, "host: ", hostname); StringUtils.appendLine(sb, indent2, "port: ", port); StringUtils.appendLine(sb, indent2, "ciphers: ", Arrays.toString(tlsCipherSuites)); StringUtils.appendLine(sb, indent2, "installed: ", getConnectionInstalled()); return sb.append(indent).append(']'); } void setSocketSendOptions(Socket s) throws SocketException { int size = s.getSendBufferSize(); if (sendBufferSize == 0) { sendBufferSize = size; } else if (sendBufferSize != size) { s.setSendBufferSize(sendBufferSize); sendBufferSize = s.getSendBufferSize(); } if (s.getTcpNoDelay() != tcpNoDelay) { s.setTcpNoDelay(tcpNoDelay); } } private void setReceiveBufferSize(Socket s) throws SocketException { int size = s.getReceiveBufferSize(); if (receiveBufferSize == 0) { receiveBufferSize = size; } else if (receiveBufferSize != size) { s.setReceiveBufferSize(receiveBufferSize); receiveBufferSize = s.getReceiveBufferSize(); } } void setReceiveBufferSize(ServerSocket ss) throws SocketException { int size = ss.getReceiveBufferSize(); if (receiveBufferSize == 0) { receiveBufferSize = size; } else if (receiveBufferSize != size) { ss.setReceiveBufferSize(receiveBufferSize); receiveBufferSize = ss.getReceiveBufferSize(); } } public void setReceiveBufferSize(DatagramSocket ds) throws SocketException { int size = ds.getReceiveBufferSize(); if (receiveBufferSize == 0) { receiveBufferSize = size; } else if (receiveBufferSize != size) { ds.setReceiveBufferSize(receiveBufferSize); receiveBufferSize = ds.getReceiveBufferSize(); } } private InetAddress hostAddr() throws UnknownHostException { if (hostAddr == null && hostname != null) hostAddr = InetAddress.getByName(hostname); return hostAddr; } private InetAddress bindAddr() throws UnknownHostException { if (bindAddress == null) return hostAddr(); if (bindAddr == null) { String resolvedBindAddress = StringUtils.replaceSystemProperties(bindAddress); bindAddr = InetAddress.getByName(resolvedBindAddress); } return bindAddr; } private InetAddress clientBindAddr() throws UnknownHostException { if (clientBindAddress == null) return hostAddr(); if (clientBindAddr == null) clientBindAddr = InetAddress.getByName(clientBindAddress); return clientBindAddr; } private List<InetAddress> blacklistAddrs() { if (blacklistAddrs == null) { blacklistAddrs = new ArrayList<InetAddress>(blacklist.length); for (String hostname : blacklist) try { blacklistAddrs.add(InetAddress.getByName(hostname)); } catch (UnknownHostException e) { LOG.warn("Failed to lookup InetAddress of " + hostname, e); } } return blacklistAddrs; } public InetSocketAddress getEndPoint() throws UnknownHostException { return new InetSocketAddress(hostAddr(), port); } public InetSocketAddress getBindPoint() throws UnknownHostException { return new InetSocketAddress(bindAddr(), port); } public InetSocketAddress getClientBindPoint() throws UnknownHostException { return new InetSocketAddress(clientBindAddr(), 0); } private void checkInstalled() { if (!isInstalled()) throw new IllegalStateException("Not installed"); } private void checkCompatible(Connection remoteConn) throws IncompatibleConnectionException { if (!isCompatible(remoteConn)) throw new IncompatibleConnectionException(remoteConn.toString()); } /** * Bind this network connection to a TCP port and start a server socket * accept loop. * * @throws IOException If there is a problem with the network interaction. * @throws GeneralSecurityException */ public synchronized boolean bind() throws IOException, GeneralSecurityException { if (!(isInstalled() && isServer())) { rebindNeeded = false; return false; } if (device == null) throw new IllegalStateException("Not attached to Device"); if (isListening()) throw new IllegalStateException("Already listening - " + listener); if (protocol.isTCP()) { TCPProtocolHandler handler = tcpHandlers.get(protocol); if (handler == null) throw new IllegalStateException("No TCP Protocol Handler for protocol " + protocol); listener = new TCPListener(this, handler); } else { UDPProtocolHandler handler = udpHandlers.get(protocol); if (handler == null) throw new IllegalStateException("No UDP Protocol Handler for protocol " + protocol); listener = new UDPListener(this, handler); } rebindNeeded = false; return true; } public final boolean isListening() { return listener != null; } public boolean isBlackListed(InetAddress ia) { return blacklistAddrs().contains(ia); } public synchronized void unbind() { Closeable tmp = listener; if (tmp == null) return; listener = null; try { tmp.close(); } catch (Throwable e) { // Ignore errors when closing the server socket. } } public Socket connect(Connection remoteConn) throws IOException, IncompatibleConnectionException, GeneralSecurityException { checkInstalled(); if (!protocol.isTCP()) throw new IllegalStateException("Not a TCP Connection"); checkCompatible(remoteConn); SocketAddress bindPoint = getClientBindPoint(); String remoteHostname = remoteConn.getHostname(); int remotePort = remoteConn.getPort(); LOG.info("Initiate connection from {} to {}:{}", bindPoint, remoteHostname, remotePort); Socket s = new Socket(); ConnectionMonitor monitor = device != null ? device.getConnectionMonitor() : null; try { s.bind(bindPoint); setReceiveBufferSize(s); setSocketSendOptions(s); String remoteProxy = remoteConn.getHttpProxy(); if (remoteProxy != null) { String userauth = null; String[] ss = StringUtils.split(remoteProxy, '@'); if (ss.length > 1) { userauth = ss[0]; remoteProxy = ss[1]; } ss = StringUtils.split(remoteProxy, ':'); int proxyPort = ss.length > 1 ? Integer.parseInt(ss[1]) : 8080; s.connect(new InetSocketAddress(ss[0], proxyPort), connectTimeout); try { getProxyManager().doProxyHandshake(s, remoteHostname, remotePort, userauth, connectTimeout); } catch (IOException e) { SafeClose.close(s); throw e; } } else { s.connect(remoteConn.getEndPoint(), connectTimeout); } if (isTls()) s = createTLSSocket(s, remoteConn); if (monitor != null) monitor.onConnectionEstablished(this, remoteConn, s); LOG.info("Established connection {}", s); return s; } catch (GeneralSecurityException e) { if (monitor != null) monitor.onConnectionFailed(this, remoteConn, s, e); SafeClose.close(s); throw e; } catch (IOException e) { if (monitor != null) monitor.onConnectionFailed(this, remoteConn, s, e); SafeClose.close(s); throw new IOException("Error while trying to establish connection "+getHostname()+" -> "+remoteHostname+":"+remotePort,e); } } public DatagramSocket createDatagramSocket() throws IOException { checkInstalled(); if (protocol.isTCP()) throw new IllegalStateException("Not a UDP Connection"); DatagramSocket ds = new DatagramSocket(getClientBindPoint()); int size = ds.getSendBufferSize(); if (sendBufferSize == 0) { sendBufferSize = size; } else if (sendBufferSize != size) { ds.setSendBufferSize(sendBufferSize); sendBufferSize = ds.getSendBufferSize(); } return ds; } public Listener getListener() { return listener; } // private void doProxyHandshake(Socket s, String hostname, int port, // String userauth, int connectTimeout) throws IOException { // // StringBuilder request = new StringBuilder(128); // request.append("CONNECT ") // .append(hostname).append(':').append(port) // .append(" HTTP/1.1\r\nHost: ") // .append(hostname).append(':').append(port); // if (userauth != null) { // byte[] b = userauth.getBytes("UTF-8"); // char[] base64 = new char[(b.length + 2) / 3 * 4]; // Base64.encode(b, 0, b.length, base64, 0); // request.append("\r\nProxy-Authorization: basic ") // .append(base64); // } // request.append("\r\n\r\n"); // OutputStream out = s.getOutputStream(); // out.write(request.toString().getBytes("US-ASCII")); // out.flush(); // // s.setSoTimeout(connectTimeout); // @SuppressWarnings("resource") // String response = new HTTPResponse(s).toString(); // s.setSoTimeout(0); // if (!response.startsWith("HTTP/1.1 2")) // throw new IOException("Unable to tunnel through " + s // + ". Proxy returns \"" + response + '\"'); // } // // private static class HTTPResponse extends ByteArrayOutputStream { // // private final String rsp; // // public HTTPResponse(Socket s) throws IOException { // super(64); // InputStream in = s.getInputStream(); // boolean eol = false; // int b; // while ((b = in.read()) != -1) { // write(b); // if (b == '\n') { // if (eol) { // rsp = new String(super.buf, 0, super.count, "US-ASCII"); // return; // } // eol = true; // } else if (b != '\r') { // eol = false; // } // } // throw new IOException("Unexpected EOF from " + s); // } // // @Override // public String toString() { // return rsp; // } // } private SSLSocket createTLSSocket(Socket s, Connection remoteConn) throws GeneralSecurityException, IOException { SSLContext sslContext = device.sslContext(); SSLSocketFactory sf = sslContext.getSocketFactory(); SSLSocket ssl = (SSLSocket) sf.createSocket(s, remoteConn.getHostname(), remoteConn.getPort(), true); ssl.setEnabledProtocols( intersect(remoteConn.tlsProtocols(), tlsProtocols())); ssl.setEnabledCipherSuites( intersect(remoteConn.tlsCipherSuites, tlsCipherSuites)); ssl.startHandshake(); return ssl; } public void close(Socket s) { LOG.info("Close connection {}", s); SafeClose.close(s); } public boolean isCompatible(Connection remoteConn) { if (remoteConn.protocol != protocol) return false; if (!protocol.isTCP()) return true; if (!isTls()) return !remoteConn.isTls(); return hasCommon(remoteConn.tlsProtocols(), tlsProtocols()) && hasCommon(remoteConn.tlsCipherSuites, tlsCipherSuites); } private boolean hasCommon(String[] ss1, String[] ss2) { for (String s1 : ss1) for (String s2 : ss2) if (s1.equals(s2)) return true; return false; } private static String[] intersect(String[] ss1, String[] ss2) { String[] ss = new String[Math.min(ss1.length, ss2.length)]; int len = 0; for (String s1 : ss1) for (String s2 : ss2) if (s1.equals(s2)) { ss[len++] = s1; break; } ; if (len == ss.length) return ss; String[] dest = new String[len]; System.arraycopy(ss, 0, dest, 0, len); return dest; } boolean equalsRDN(Connection other) { return commonName != null ? commonName.equals(other.commonName) : other.commonName == null && hostname.equals(other.hostname) && port == other.port && protocol == other.protocol; } public String getOlockHash() { return olockHash; } public void setOlockHash(String olockHash) { this.olockHash = olockHash; } void reconfigure(Connection from) { setOlockHash(from.olockHash); setUuid(from.uuid); setCommonName(from.commonName); setHostname(from.hostname); setPort(from.port); setBindAddress(from.bindAddress); setClientBindAddress(from.clientBindAddress); setProtocol(from.protocol); setHttpProxy(from.httpProxy); setBacklog(from.backlog); setConnectTimeout(from.connectTimeout); setRequestTimeout(from.requestTimeout); setAcceptTimeout(from.acceptTimeout); setReleaseTimeout(from.releaseTimeout); setResponseTimeout(from.responseTimeout); setRetrieveTimeout(from.retrieveTimeout); setIdleTimeout(from.idleTimeout); setSocketCloseDelay(from.socketCloseDelay); setSendBufferSize(from.sendBufferSize); setReceiveBufferSize(from.receiveBufferSize); setSendPDULength(from.sendPDULength); setReceivePDULength(from.receivePDULength); setMaxOpsPerformed(from.maxOpsPerformed); setMaxOpsPerformed(from.maxOpsInvoked); setPackPDV(from.packPDV); setTcpNoDelay(from.tcpNoDelay); setTlsNeedClientAuth(from.tlsNeedClientAuth); setTlsCipherSuites(from.tlsCipherSuites); setTlsProtocols(from.tlsProtocols); setBlacklist(from.blacklist); setConnectionInstalled(from.connectionInstalled); reconfigureExtensions(from); } }