package com.hwlcn.ldap.ldap.sdk; import java.io.BufferedInputStream; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import com.hwlcn.ldap.asn1.ASN1Exception; import com.hwlcn.ldap.asn1.ASN1StreamReader; import com.hwlcn.ldap.ldap.protocol.LDAPMessage; import com.hwlcn.ldap.ldap.protocol.LDAPResponse; import com.hwlcn.ldap.ldap.sdk.extensions.NoticeOfDisconnectionExtendedResult; import com.hwlcn.ldap.util.DebugType; import com.hwlcn.core.annotation.InternalUseOnly; import com.hwlcn.ldap.util.WakeableSleeper; import static com.hwlcn.ldap.ldap.sdk.LDAPMessages.*; import static com.hwlcn.ldap.util.Debug.*; import static com.hwlcn.ldap.util.StaticUtils.*; @InternalUseOnly() final class LDAPConnectionReader extends Thread { private static final int DEFAULT_INPUT_BUFFER_SIZE = 4096; private volatile ASN1StreamReader asn1StreamReader; private volatile boolean closeRequested; private final ConcurrentHashMap<Integer,ResponseAcceptor> acceptorMap; private volatile Exception startTLSException; private volatile InputStream inputStream; private volatile OutputStream startTLSOutputStream; private final LDAPConnection connection; private volatile Socket socket; private volatile SSLContext sslContext; private volatile Thread thread; private final WakeableSleeper startTLSSleeper; LDAPConnectionReader(final LDAPConnection connection, final LDAPConnectionInternals connectionInternals) throws IOException { this.connection = connection; setName(constructThreadName(connectionInternals)); setDaemon(true); socket = connectionInternals.getSocket(); inputStream = new BufferedInputStream(socket.getInputStream(), DEFAULT_INPUT_BUFFER_SIZE); asn1StreamReader = new ASN1StreamReader(inputStream, connection.getConnectionOptions().getMaxMessageSize()); acceptorMap = new ConcurrentHashMap<Integer,ResponseAcceptor>(); closeRequested = false; sslContext = null; startTLSException = null; startTLSOutputStream = null; startTLSSleeper = new WakeableSleeper(); if (! connectionInternals.synchronousMode()) { final LDAPConnectionOptions options = connection.getConnectionOptions(); final int connectTimeout = options.getConnectTimeoutMillis(); if (connectTimeout > 0) { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to connect timeout of " + connectTimeout + "ms in LDAPConnectionReader constructor"); } socket.setSoTimeout(connectTimeout); } else { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to 0ms in LDAPConnectionReader " + "constructor"); } socket.setSoTimeout(0); } if (socket instanceof SSLSocket) { final SSLSocket sslSocket = (SSLSocket) socket; sslSocket.startHandshake(); } } } void registerResponseAcceptor(final int messageID, final ResponseAcceptor acceptor) throws LDAPException { if (acceptorMap.putIfAbsent(messageID, acceptor) != null) { throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_CONNREADER_MSGID_IN_USE.get()); } } void deregisterResponseAcceptor(final int messageID) { acceptorMap.remove(messageID); } int getActiveOperationCount() { return acceptorMap.size(); } @Override() public void run() { boolean reconnect = false; thread = Thread.currentThread(); while (! closeRequested) { try { final LDAPResponse response; try { response = LDAPMessage.readLDAPResponseFrom(asn1StreamReader, true, connection.getCachedSchema()); } catch (LDAPException le) { final Throwable t = le.getCause(); if ((t != null) && (t instanceof SocketTimeoutException)) { final SocketTimeoutException ste = (SocketTimeoutException) t; debugException(Level.FINEST, ste); if (sslContext != null) { try { final int connectTimeout = connection.getConnectionOptions(). getConnectTimeoutMillis(); if (connectTimeout > 0) { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to connect timeout of " + connectTimeout + "ms in " + "LDAPConnectionReader.run while performing " + "StartTLS processing."); } socket.setSoTimeout(connectTimeout); } else { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to 0ms in " + "LDAPConnectionReader.run while performing " + "StartTLS processing."); } socket.setSoTimeout(0); } final SSLSocket sslSocket; final SSLSocketFactory socketFactory = sslContext.getSocketFactory(); synchronized (socketFactory) { sslSocket = (SSLSocket) socketFactory.createSocket(socket, connection.getConnectedAddress(), socket.getPort(), true); sslSocket.startHandshake(); } inputStream = new BufferedInputStream(sslSocket.getInputStream(), DEFAULT_INPUT_BUFFER_SIZE); asn1StreamReader = new ASN1StreamReader(inputStream, connection.getConnectionOptions().getMaxMessageSize()); startTLSOutputStream = sslSocket.getOutputStream(); socket = sslSocket; startTLSSleeper.wakeup(); } catch (Exception e) { debugException(e); connection.setDisconnectInfo(DisconnectType.SECURITY_PROBLEM, getExceptionMessage(e), e); startTLSException = e; closeRequested = true; if (thread != null) { thread.setName(thread.getName() + " (closed)"); thread = null; } closeInternal(true, getExceptionMessage(e)); startTLSSleeper.wakeup(); return; } sslContext = null; } continue; } if (closeRequested || connection.closeRequested() || (connection.getDisconnectType() != null)) { closeRequested = true; debugException(Level.FINEST, le); } else { debugException(le); } final String message; Level debugLevel = Level.SEVERE; if (t == null) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, le.getMessage(), t); message = le.getMessage(); debugLevel = Level.WARNING; } else if ((t instanceof InterruptedIOException) && socket.isClosed()) { connection.setDisconnectInfo( DisconnectType.SERVER_CLOSED_WITHOUT_NOTICE, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_INTERRUPTED_IO.get( connection.getHostPort()); debugLevel = Level.WARNING; } else if (t instanceof IOException) { connection.setDisconnectInfo(DisconnectType.IO_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); debugLevel = Level.WARNING; } else if (t instanceof ASN1Exception) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); } else { connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); } debug(debugLevel, DebugType.LDAP, message, t); if ((! closeRequested) && connection.getConnectionOptions().autoReconnect()) { reconnect = true; break; } else { closeRequested = true; if (thread != null) { thread.setName(thread.getName() + " (closed)"); thread = null; } closeInternal(true, message); return; } } if (response == null) { connection.setDisconnectInfo( DisconnectType.SERVER_CLOSED_WITHOUT_NOTICE, null, null); if ((! closeRequested) && (! connection.unbindRequestSent()) && connection.getConnectionOptions().autoReconnect()) { reconnect = true; break; } else { closeRequested = true; if (thread != null) { thread.setName(thread.getName() + " (closed)"); thread = null; } closeInternal(true, null); return; } } debugLDAPResult(response, connection); final ResponseAcceptor responseAcceptor; if ((response instanceof SearchResultEntry) || (response instanceof SearchResultReference)) { responseAcceptor = acceptorMap.get(response.getMessageID()); } else if (response instanceof IntermediateResponse) { final IntermediateResponse ir = (IntermediateResponse) response; responseAcceptor = acceptorMap.get(response.getMessageID()); IntermediateResponseListener l = null; if (responseAcceptor instanceof LDAPRequest) { final LDAPRequest r = (LDAPRequest) responseAcceptor; l = r.getIntermediateResponseListener(); } else if (responseAcceptor instanceof IntermediateResponseListener) { l = (IntermediateResponseListener) responseAcceptor; } if (l == null) { debug(Level.WARNING, DebugType.LDAP, WARN_INTERMEDIATE_RESPONSE_WITH_NO_LISTENER.get( String.valueOf(ir))); } else { try { l.intermediateResponseReturned(ir); } catch (Exception e) { debugException(e); } } continue; } else { responseAcceptor = acceptorMap.remove(response.getMessageID()); } if (responseAcceptor == null) { if ((response instanceof ExtendedResult) && (response.getMessageID() == 0)) { ExtendedResult extendedResult = (ExtendedResult) response; final String oid = extendedResult.getOID(); if (NoticeOfDisconnectionExtendedResult. NOTICE_OF_DISCONNECTION_RESULT_OID.equals(oid)) { extendedResult = new NoticeOfDisconnectionExtendedResult( extendedResult); connection.setDisconnectInfo( DisconnectType.SERVER_CLOSED_WITH_NOTICE, extendedResult.getDiagnosticMessage(), null); } else if ("1.3.6.1.4.1.30221.2.6.5".equals(oid)) { try { final Class<?> c = Class.forName("com.hwlcn.ldap.ldap.sdk." + "unboundidds.extensions." + "InteractiveTransactionAbortedExtendedResult"); final Constructor<?> ctor = c.getConstructor(ExtendedResult.class); extendedResult = (ExtendedResult) ctor.newInstance(extendedResult); } catch (Exception e) { debugException(e); } } final UnsolicitedNotificationHandler handler = connection.getConnectionOptions(). getUnsolicitedNotificationHandler(); if (handler == null) { if (debugEnabled(DebugType.LDAP)) { debug(Level.WARNING, DebugType.LDAP, WARN_READER_UNHANDLED_UNSOLICITED_NOTIFICATION.get( response)); } } else { handler.handleUnsolicitedNotification(connection, extendedResult); } continue; } if (debugEnabled(DebugType.LDAP)) { debug(Level.WARNING, DebugType.LDAP, WARN_READER_NO_ACCEPTOR.get(response)); } continue; } try { responseAcceptor.responseReceived(response); } catch (LDAPException le) { debugException(le); debug(Level.WARNING, DebugType.LDAP, ERR_READER_ACCEPTOR_ERROR.get(String.valueOf(response), connection.getHostPort(), getExceptionMessage(le)), le); } } catch (Exception e) { debugException(e); final String message; Level debugLevel = Level.SEVERE; if (e instanceof IOException) { connection.setDisconnectInfo(DisconnectType.IO_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); debugLevel = Level.WARNING; } else if (e instanceof ASN1Exception) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); } else { connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); } debug(debugLevel, DebugType.LDAP, message, e); if (connection.getConnectionOptions().autoReconnect()) { reconnect = true; break; } else { closeRequested = true; if (thread != null) { thread.setName(thread.getName() + " (closed)"); thread = null; } closeInternal(true, message); return; } } } if (thread != null) { thread.setName(constructThreadName(null)); thread = null; } if (reconnect && (! connection.closeRequested())) { try { connection.setNeedsReconnect(); } catch (Exception e) { debugException(e); } } else { closeInternal(true, null); } } LDAPResponse readResponse(final int messageID) throws LDAPException { while (true) { try { final LDAPResponse response = LDAPMessage.readLDAPResponseFrom( asn1StreamReader, false, connection.getCachedSchema()); if (response == null) { return new ConnectionClosedResponse(ResultCode.SERVER_DOWN, null); } if (response.getMessageID() == messageID) { return response; } if ((response instanceof ExtendedResult) && (response.getMessageID() == 0)) { ExtendedResult extendedResult = (ExtendedResult) response; final String oid = extendedResult.getOID(); if (NoticeOfDisconnectionExtendedResult. NOTICE_OF_DISCONNECTION_RESULT_OID.equals(oid)) { extendedResult = new NoticeOfDisconnectionExtendedResult( extendedResult); connection.setDisconnectInfo( DisconnectType.SERVER_CLOSED_WITH_NOTICE, extendedResult.getDiagnosticMessage(), null); } else if ("1.3.6.1.4.1.30221.2.6.5".equals(oid)) { try { final Class<?> c = Class.forName("com.hwlcn.ldap.ldap.sdk." + "unboundidds.extensions." + "InteractiveTransactionAbortedExtendedResult"); final Constructor<?> ctor = c.getConstructor(ExtendedResult.class); extendedResult = (ExtendedResult) ctor.newInstance(extendedResult); } catch (Exception e) { debugException(e); } } final UnsolicitedNotificationHandler handler = connection.getConnectionOptions(). getUnsolicitedNotificationHandler(); if (handler == null) { if (debugEnabled(DebugType.LDAP)) { debug(Level.WARNING, DebugType.LDAP, WARN_READER_UNHANDLED_UNSOLICITED_NOTIFICATION.get( response)); } } else { handler.handleUnsolicitedNotification(connection, extendedResult); } continue; } if (debugEnabled(DebugType.LDAP)) { debug(Level.WARNING, DebugType.LDAP, WARN_READER_DISCARDING_UNEXPECTED_RESPONSE.get(response, messageID)); } } catch (LDAPException le) { debugException(le); final Throwable t = le.getCause(); if ((t != null) && (t instanceof SocketTimeoutException)) { throw new LDAPException(ResultCode.TIMEOUT, le.getMessage(), le); } final String message; Level debugLevel = Level.SEVERE; if (t == null) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, le.getMessage(), t); message = le.getMessage(); debugLevel = Level.WARNING; } else if (t instanceof IOException) { connection.setDisconnectInfo(DisconnectType.IO_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); debugLevel = Level.WARNING; } else if (t instanceof ASN1Exception) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); } else { connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, le.getMessage(), t); message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(t)); } debug(debugLevel, DebugType.LDAP, message, t); if (! connection.getConnectionOptions().autoReconnect()) { closeRequested = true; } closeInternal(true, message); throw le; } catch (Exception e) { debugException(e); final String message; Level debugLevel = Level.SEVERE; if (e instanceof IOException) { connection.setDisconnectInfo(DisconnectType.IO_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); debugLevel = Level.WARNING; } else if (e instanceof ASN1Exception) { connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); } else { connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, null, e); message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get( connection.getHostPort(), getExceptionMessage(e)); } debug(debugLevel, DebugType.LDAP, message, e); if (! connection.getConnectionOptions().autoReconnect()) { closeRequested = true; } closeInternal(true, message); throw new LDAPException(ResultCode.SERVER_DOWN, message, e); } } } void setSoTimeout(final int soTimeout) throws LDAPException { try { socket.setSoTimeout(soTimeout); } catch (final Exception e) { debugException(e); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_READER_CANNOT_SET_SO_TIMEOUT.get(soTimeout, connection.toString(), getExceptionMessage(e)), e); } } OutputStream doStartTLS(final SSLContext sslContext) throws LDAPException { if (connection.synchronousMode()) { try { final int connectTimeout = connection.getConnectionOptions(). getConnectTimeoutMillis(); if (connectTimeout > 0) { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to connect timeout of " + connectTimeout + "ms in " + "LDAPConnectionReader.doStartTLS while performing " + "StartTLS processing."); } socket.setSoTimeout(connectTimeout); } else { if (debugEnabled()) { debug(Level.INFO, DebugType.CONNECT, "Setting SO_TIMEOUT to 0ms in " + "LDAPConnectionReader.doStartTLS while performing " + "StartTLS processing."); } socket.setSoTimeout(0); } final SSLSocket sslSocket; final SSLSocketFactory socketFactory = sslContext.getSocketFactory(); synchronized (socketFactory) { sslSocket = (SSLSocket) socketFactory.createSocket(socket, connection.getConnectedAddress(), socket.getPort(), true); sslSocket.startHandshake(); } inputStream = new BufferedInputStream(sslSocket.getInputStream(), DEFAULT_INPUT_BUFFER_SIZE); asn1StreamReader = new ASN1StreamReader(inputStream, connection.getConnectionOptions().getMaxMessageSize()); startTLSOutputStream = sslSocket.getOutputStream(); socket = sslSocket; final OutputStream outputStream = startTLSOutputStream; startTLSOutputStream = null; return outputStream; } catch (Exception e) { debugException(e); connection.setDisconnectInfo(DisconnectType.SECURITY_PROBLEM, getExceptionMessage(e), e); startTLSException = e; closeRequested = true; closeInternal(true, getExceptionMessage(e)); throw new LDAPException(ResultCode.SERVER_DOWN, ERR_CONNREADER_STARTTLS_FAILED.get(getExceptionMessage(e)), e); } } else { this.sslContext = sslContext; while (true) { if (startTLSOutputStream != null) { final OutputStream outputStream = startTLSOutputStream; startTLSOutputStream = null; return outputStream; } else if (thread == null) { if (startTLSException == null) { throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_CONNREADER_STARTTLS_FAILED_NO_EXCEPTION.get()); } else { final Exception e = startTLSException; startTLSException = null; throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_CONNREADER_STARTTLS_FAILED.get(getExceptionMessage(e)), e); } } startTLSSleeper.sleep(10); } } } void close(final boolean notifyConnection) { closeRequested = true; for (int i=0; i < 5; i++) { try { final Thread t = thread; if ((t == null) || (t == Thread.currentThread()) || (! t.isAlive())) { break; } else { t.interrupt(); t.join(100L); } } catch (Exception e) { debugException(e); } } closeInternal(notifyConnection, null); } private void closeInternal(final boolean notifyConnection, final String message) { final InputStream is = inputStream; inputStream = null; try { if (is != null) { is.close(); } } catch (Exception e) { debugException(e); } if (notifyConnection) { connection.setClosed(); } final Iterator<Integer> iterator = acceptorMap.keySet().iterator(); while (iterator.hasNext()) { final int messageID = iterator.next(); final ResponseAcceptor acceptor = acceptorMap.get(messageID); try { if (message == null) { final DisconnectType disconnectType = connection.getDisconnectType(); if (disconnectType == null) { acceptor.responseReceived(new ConnectionClosedResponse( ResultCode.SERVER_DOWN, null)); } else { acceptor.responseReceived(new ConnectionClosedResponse( disconnectType.getResultCode(), connection.getDisconnectMessage())); } } else { acceptor.responseReceived(new ConnectionClosedResponse( ResultCode.SERVER_DOWN, message)); } } catch (Exception e) { debugException(e); } iterator.remove(); } } Thread getReaderThread() { return thread; } void updateThreadName() { final Thread t = thread; if (t != null) { try { t.setName(constructThreadName(connection.getConnectionInternals(true))); } catch (final Exception e) { debugException(e); } } } private String constructThreadName( final LDAPConnectionInternals connectionInternals) { final StringBuilder buffer = new StringBuilder(); buffer.append("Connection reader for connection "); buffer.append(connection.getConnectionID()); buffer.append(' '); String name = connection.getConnectionName(); if (name != null) { buffer.append('\''); buffer.append(name); buffer.append("' "); } name = connection.getConnectionPoolName(); if (name != null) { buffer.append("in pool '"); buffer.append(name); buffer.append("' "); } if (connectionInternals == null) { buffer.append("(not connected)"); } else { buffer.append("to "); buffer.append(connectionInternals.getHost()); buffer.append(':'); buffer.append(connectionInternals.getPort()); } return buffer.toString(); } }