package uc.protocols; import helpers.GH; import helpers.LockedRunnable; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.UnresolvedAddressException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import org.apache.log4j.Logger; import org.eclipse.core.runtime.Platform; import logger.LoggerFactory; import uc.DCClient; import uc.ICryptoManager; import uc.crypto.HashValue; import uc.protocols.MultiStandardConnection.IUnblocking; /** * Good example for bad looking ugly code that somehow more or less works... * * * @author Quicksilver * */ public class UnblockingConnection extends AbstractConnection implements IUnblocking { private static final Logger logger = LoggerFactory.make(); /** * here we store IP addresses of other clients that failed DHE key exchange with us.. */ private static final Set<InetAddress> problematic = Collections.synchronizedSet(new HashSet<InetAddress>()); // private static final Set<InetAddress> invalidEncodedCerts = //TODO this would be nice as workaround for those invalid certs // Collections.synchronizedSet(new HashSet<InetAddress>()); private final boolean encryption; private final boolean serverSide; private volatile SSLEngine engine; private volatile HashValue fingerPrint; private final AtomicBoolean connectSent = new AtomicBoolean(false); private final AtomicBoolean disconnectSent = new AtomicBoolean(false); // private final Semaphore disconnectSentSem = new Semaphore(1); private volatile SelectionKey key; private volatile boolean blocking = false; private static final Object inetAddySynch = new Object(); private volatile InetSocketAddress inetAddress = null ; // private final Object bufferLock = new Object(); private final Lock bufferLock = new ReentrantLock(); private final Condition bytesChanged = bufferLock.newCondition(); private volatile ByteBuffer byteBuffer = ByteBuffer.allocate(1024*2); //in Buffer.. // private final CharBuffer outcharBuffer = CharBuffer.allocate(1024); // private final ByteBuffer outBuffer = ByteBuffer.allocate(outcharBuffer.capacity()*4); private ByteBuffer encrypting, decrypting; //bytebuffers for encrypting and decrypting data.. private final VarByteBuffer varOutBuffer = new VarByteBuffer(1024*8); //replaces the outgoing stringbuffer.. private final VarByteBuffer varInBuffer = new VarByteBuffer(1024*8); private final Semaphore semaphore = new Semaphore(0,false); private Object target; // socketchannel... or hubaddy in string format /** * used for client as well as server mode. */ public UnblockingConnection(ICryptoManager cryptoManager,SocketChannel soChan, ConnectionProtocol connectionProt,boolean encryption, boolean serverSide,HashValue fingerPrint) { super(cryptoManager,connectionProt); this.encryption = encryption; this.serverSide = serverSide; this.target = soChan; this.fingerPrint = fingerPrint; } /** * only used for client mode * @param addy * @param connectionProt * @param encryption * @param allowDH - can forbid DH keys due to problems with DH and other clients... especially apex.. */ public UnblockingConnection(ICryptoManager cryptoManager,String addy, ConnectionProtocol connectionProt,boolean encryption,HashValue fingerPrint) { super(cryptoManager,connectionProt); this.encryption = encryption; this.target = addy; serverSide = false; this.fingerPrint = fingerPrint; } public void setKey(SelectionKey key) { this.key = key; } public void start() { //todo ... if Proxy in use start connect should be in seperate thread.. if (target instanceof String) { reset((String)target); } else if (target instanceof SocketChannel) { reset((SocketChannel)target); } else if (target instanceof InetSocketAddress) { reset((InetSocketAddress)target); } } public UnblockingConnection(ICryptoManager cryptoManager,InetSocketAddress isa, ConnectionProtocol connectionProt,boolean encryption,HashValue fingerPrint){ super(cryptoManager,connectionProt); this.target = isa; this.encryption = encryption; this.serverSide = false; this.fingerPrint = fingerPrint; } /** * @param op - marks the key if it is valid for the provided op */ private void addInterestOp(final int op) { MultiStandardConnection.get().asynchExec(new Runnable() { public void run() { if (key != null && key.isValid()) { key.interestOps(key.interestOps()| op); } } }); } public void send(ByteBuffer toSend) { bufferLock.lock(); try { if (encryption) { try { int lastRemaining; do { lastRemaining = toSend.remaining(); encrypting.clear(); SSLEngineResult ssler = engine.wrap(toSend, encrypting); encrypting.flip(); varOutBuffer.putBytes(encrypting); evaluateHandshakeStatus(ssler.getHandshakeStatus()); } while (toSend.hasRemaining() && lastRemaining != toSend.remaining()); //even empty buffers need to execute this at least once.. } catch(RuntimeException re) { addProblematic(re); } catch(SSLException ssle) { logger.debug(ssle, ssle); } } else { varOutBuffer.putBytes(toSend); } } finally { bufferLock.unlock(); } if (!blocking) { addInterestOp(SelectionKey.OP_WRITE); } } public void write() throws IOException { bufferLock.lock(); try { int numBytesWritten = 0; if (varOutBuffer.hasRemaining()) { SocketChannel sochan = (SocketChannel) key.channel(); //outBuffer.flip(); numBytesWritten = varOutBuffer.writeToChannel(sochan); logger.debug("written " + numBytesWritten); if (numBytesWritten == -1) { GH.close(sochan); onDisconnect(); } } if (numBytesWritten == 0) { key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE)); // logger.debug("no more write interest"); } } finally { bufferLock.unlock(); } } public void onDisconnect() throws IOException{ /* if (printLifetime) { synchronized(bufferLock) { logger.debug("Lifetime in Seconds: "+ ((System.currentTimeMillis()-connectionCreated)/1000)+ " "+varInBuffer.remaining()); } } */ logger.debug("onDisconnect()"); if (disconnectSent.compareAndSet(false, true)) { // disconnectSent = true; key.cancel();//unregister with selector SocketChannel sochan = (SocketChannel)key.channel(); sochan.close(); DCClient.execute(new LockedRunnable(cp.writeLock()) { @Override protected void lockedRun() { try { cp.onDisconnect(); } catch(IOException ioe) { logger.error(ioe,ioe); } } }); } } /** * the io thread reads here bytes from the socket channel * converts to chars and then passes the gained string * to read(String read) for further work * * @throws IOException */ public void read() throws IOException { SocketChannel sochan =(SocketChannel)key.channel(); bufferLock.lock(); try { if (varInBuffer.remaining() > 250 * 1024) { //if there is more than 250kiB of data lying around in the VarInBuffer.. //-> stop reading -> security (nobody can flood us while we write) + performance gain (Buffer grows too large) return; } } finally { bufferLock.unlock(); } int numBytesRead = sochan.read(byteBuffer); if (numBytesRead >= 0) { bufferLock.lock(); try { if (encryption) { unwrap(); } else { byteBuffer.flip(); varInBuffer.putBytes(byteBuffer); byteBuffer.clear(); } checkRead(); } finally { bufferLock.unlock(); } } else { GH.close(sochan); onDisconnect(); } } /** * checks if reading is possible and reads if possible.. * must be done while holding bufferlock */ private void checkRead() { if (varInBuffer.hasRemaining() && semaphore.tryAcquire()) { DCClient.execute(new Runnable(){ public void run() { try { if (!connectSent.get() && Platform.inDevelopmentMode()) { //DEBUG remove logger.warn("connect not sent: "+varInBuffer.toString()); } processread(); } finally { semaphore.release(); } } }); } } /** * signals on connect if not already done so.. */ private void signalOnConnect() { if (connectSent.compareAndSet(false, true)) { //connectSent = true; DCClient.execute(new Runnable() { public void run() { Lock l = cp.writeLock(); l.lock(); try { if (cp.getState() == ConnectionState.CONNECTING) { if (getInetSocketAddress() != null) { // check one last time for socket addy being set.. cp.onConnect(); semaphore.release(); } else { asynchClose(); } } } catch (IOException ioe) { logger.debug(ioe, ioe); } finally { l.unlock(); } bufferLock.lock(); try { checkRead(); } finally { bufferLock.unlock(); } } }); } } private void evaluateHandshakeStatus(HandshakeStatus status) { logger.debug(status); switch (status) { case FINISHED: case NOT_HANDSHAKING: boolean tryValidation = fingerPrint != null && !connectSent.get(); if (tryValidation && !checkFingerprint()) { return; } signalOnConnect(); break; case NEED_WRAP: send(ByteBuffer.allocate(0)); break; case NEED_TASK: DCClient.execute(new Runnable() { public void run() { Runnable r; while ((r = engine.getDelegatedTask()) != null) { r.run(); logger.debug("executing task"); } logger.debug("Status after task: "+engine.getHandshakeStatus()); evaluateHandshakeStatus(engine.getHandshakeStatus()); } }); break; case NEED_UNWRAP: bufferLock.lock(); try { unwrap(); } finally { bufferLock.unlock(); } break; } } /* * */ private void unwrap() { int positionLast = 0; while (byteBuffer.position() != positionLast) { // just taking remaining doesn't work sometimes (bytes are in line but can't be decoded) positionLast = byteBuffer.position(); byteBuffer.flip(); SSLEngineResult ssler = null; try { decrypting.clear(); ssler = engine.unwrap(byteBuffer, decrypting); decrypting.flip(); //logger.debug("bytes after encryption: "+decrypting.remaining()+" "+ssler); varInBuffer.putBytes(decrypting); } catch (Exception e) { if ((e.toString().contains("Cipher buffering error")|| e.toString().contains("record too big") )&& Platform.inDevelopmentMode()) { logger.warn(cp.toString(),e); } addProblematic(e); asynchClose(); } byteBuffer.compact(); if (ssler != null && !ssler.getHandshakeStatus().equals(HandshakeStatus.NEED_UNWRAP)) {// check against need unwrap prevents recursiveness.. -> we do further unwrapping here anyway.. evaluateHandshakeStatus(ssler.getHandshakeStatus()); } } } /* * adds a user to a problematic set so no DH is used with that user 178.9.55.201 * workaround for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6521495 */ private void addProblematic(Exception e) { synchronized(inetAddySynch) { if (inetAddress != null) { boolean contained = problematic.contains(inetAddress.getAddress()); problematic.add(inetAddress.getAddress()); if (e.toString().contains("Invalid encoding: zero length Int value")) { // invalidEncodedCerts.add(inetAddress.getAddress()); } if (Platform.inDevelopmentMode()) { if (e.toString().contains("unknown_ca") || e.toString().contains("Invalid encoding: zero length Int value")) { logger.info(e+" "+inetAddress.getAddress()+" probcontains: "+contained); } else { logger.warn(e+" "+inetAddress.getAddress()+" probcontains: "+contained,e); } } } } } private boolean trySetInetAddy(SocketChannel sochan) { synchronized (inetAddySynch) { if (getInetSocketAddress() != null) { return true; } return !((inetAddress = (InetSocketAddress)sochan.socket().getRemoteSocketAddress()) == null || inetAddress.getAddress() == null); } } /** * when a socket has finished connecting * this method is called by MultiStandardConnection * no one else may call it.. * * */ public void connected() { disconnectSent.set(false); final SocketChannel sochan =(SocketChannel)key.channel(); trySetInetAddy(sochan); DCClient.execute(new Runnable(){ public void run() { long startconnect = System.currentTimeMillis(); try { while (!trySetInetAddy(sochan) && sochan.isOpen()) { // if (sochan.isConnectionPending()) { sochan.finishConnect(); // } GH.sleep(50); if (startconnect + cp.getSocketTimeout() < System.currentTimeMillis()) { sochan.close(); } } if (sochan.isOpen() && cp.getState() == ConnectionState.CONNECTING) { synchronized (inetAddySynch) { logger.debug("connected to: "+inetAddress.getAddress().getHostAddress()); } if (encryption) { synchronized (inetAddySynch) { if (problematic.contains(inetAddress.getAddress()) || !(target instanceof String) ) { List<String> s = Arrays.asList(engine.getEnabledCipherSuites()); s = GH.filter(s, "_DHE_"); s = GH.filter(s, "_ECDH_"); s = GH.filter(s, "_ECDHE_"); s = GH.filter(s, "_DH_"); engine.setEnabledCipherSuites(s.toArray(new String[]{})); logger.debug("enabled ciphers: "+GH.toString(engine.getEnabledCipherSuites())); } } send(ByteBuffer.allocate(0)); //send an empty message to start handshake.. } else { signalOnConnect(); } } } catch(IOException ioe) { logger.error(ioe,ioe); close(); } } }); } /** * when IO-thread has transfered read data to a string * this method will be called from a separate thread and * process/divide the string to suitable pieces for the * ConnectionProtocol */ private void processread() { boolean stopp = false; do { byte[] sBa; bufferLock.lock(); try { sBa = varInBuffer.readUntil((byte) cp.getCommandStopByte()); if (sBa.length == 0) { return; } } finally { bufferLock.unlock(); } Lock l = cp.writeLock(); l.lock(); try { cp.receivedCommand(sBa); } catch (IOException ioe) { close(); logger.debug(ioe,ioe); } catch (RuntimeException re) { close(); logger.warn(re,re); } finally { l.unlock(); } bufferLock.lock(); try { stopp = !varInBuffer.hasRemaining(); } finally { bufferLock.unlock(); } } while (!stopp); } /** * resets the connection with the given channel ... can be called on reconnection to the same hub for example.. * @param soChan * @param connectionProt */ public void reset(SocketChannel soChan) { semaphore.drainPermits(); // disconnectSent = false; disconnectSent.set(false); // disconnectSentSem.drainPermits(); // disconnectSentSem.release(); connectSent.set(false); logger.debug("in reset(sochan)"); // outBuffer.clear(); // outBuffer.flip(); //nothing available in the outBuffer.. byteBuffer.clear(); //charBuffer.clear(); //outgoingBuffer.clear(); synchronized (varOutBuffer) { varOutBuffer.clear(); } if (encryption) { bufferLock.lock(); try { engine = cryptoManager.createSSLEngine(); engine.setUseClientMode(!serverSide); if (serverSide) { engine.setNeedClientAuth(true); engine.setWantClientAuth(true); engine.setEnableSessionCreation(true); } List<String> enabledCS = Arrays.asList(engine .getSupportedCipherSuites()); /* * disabled: * MD5: Old hashfunction and broken. * RC4: Old streamcipher and no longer trustable * Kerberos: needs server.. probably special settings.. nobody will use it * SSL: Enforces use of TLS */ enabledCS = GH.filter(enabledCS, "MD5", "RC4", "KRB5", "SSL","NULL"); if (!enabledCS.isEmpty()) { engine.setEnabledCipherSuites(enabledCS .toArray(new String[] {})); } List<String> enabledProt = Arrays.asList(engine .getSupportedProtocols()); enabledProt = GH.filter(enabledProt, "SSL"); if (!enabledProt.isEmpty()) { engine.setEnabledProtocols(enabledProt .toArray(new String[] {})); } SSLSession ssle = engine.getSession(); byteBuffer = ByteBuffer.allocate(ssle .getPacketBufferSize()); decrypting = ByteBuffer.allocate(ssle .getApplicationBufferSize()); encrypting = ByteBuffer.allocate(ssle .getPacketBufferSize()); logger.debug("encrypted connection created"); } catch (RuntimeException e) { logger.error(e, e); } finally { bufferLock.unlock(); } } Lock l = cp.writeLock(); l.lock(); try { cp.beforeConnect(); } finally { l.unlock(); } logger.debug("after registering reset(sochan)"); MultiStandardConnection.get().register(soChan, this,false); logger.debug("after registering reset(sochan)"); } @Override public void reset(final InetSocketAddress addy ) { logger.debug("in reset("+addy+")"); try { final SocketChannel sochan = SocketChannel.open(); AbstractConnection.bindSocket( sochan); logger.debug("in reset openedChanel"); sochan.configureBlocking(false); //important ... so we can immediately go on here logger.debug("in reset configured not Blocking"); //set preferences in performance.. int[] pp = cp.getPerformancePrefs(); if (pp != null) { sochan.socket().setPerformancePreferences(pp[0], pp[1], pp[2]); } if (!Socks.isEnabled()) { sochan.connect( addy ); reset(sochan); } else { DCClient.execute(new Runnable() { public void run() { try { Socks.getDefaultSocks().connect(sochan, addy); sochan.configureBlocking(false); reset(sochan); } catch(IOException e){ logger.warn(e, e); } } }); } } catch (UnresolvedAddressException uae) { logger.debug(uae); } catch(IOException e){ logger.warn(e + " addy: "+addy, e); } } /** * will close if keyprint is not correct .. * @return true if correct.. false otherwise.. */ private boolean checkFingerprint() { if (fingerPrint == null) { throw new IllegalStateException(); } Certificate cert = null; SSLSession ssle = engine.getSession(); boolean correct = false; try { cert = ssle.getPeerCertificates()[0]; // ssle.getPeerCertificateChain()[0]; if (fingerPrint != null) { HashValue hash = HashValue.createHash( cert.getEncoded(), fingerPrint.magnetString()); correct = hash.equals(fingerPrint); // logger.debug("fingerprint correct: "+ correct +" "+hash); // logger.debug("sha-1 fingerprint: "+ GH.getHex( GH.getHash(cert.getEncoded(),"SHA-1"))); if (!correct) { logger.info("Bad Fingerprint found from "+getInetSocketAddress() +"\nFound: "+hash +"\nExpected: "+fingerPrint); cp.keyPrintFailed(); } } } catch (SSLPeerUnverifiedException e) { logger.error(getInetSocketAddress()+ " did not present a valid certificate."); } catch (CertificateEncodingException e) { logger.warn(e,e); } if (!correct) { asynchClose(); } return correct; } public boolean setFingerPrint(HashValue hash) { if (encryption && connectSent.get() && this.fingerPrint == null) { this.fingerPrint = hash; return checkFingerprint(); } else { this.fingerPrint = hash; return true; } } public boolean isFingerPrintUsed() { return encryption && fingerPrint != null; } public void getCryptoInfo(ICryptoInfo info) { if (!encryption) { throw new IllegalStateException(); } info.setInfo(engine); } private void asynchClose() { DCClient.execute(new Runnable() { public void run() { close(); } }); } @Override public void close() { final Semaphore sem = new Semaphore(0); Runnable r = new Runnable() { public void run() { if (key != null) { if (key.channel().isOpen()) { if (encryption) { engine.closeOutbound(); send(ByteBuffer.allocate(0)); //used for wrapping remaining data.. } flush(encryption?800:400); } GH.close(key.channel()); key.cancel(); try { if (!key.isValid()) { onDisconnect(); } } catch(IOException ioe){ logger.warn(ioe, ioe); } } sem.release(); } }; DCClient.execute(r); sem.acquireUninterruptibly(); } // private final Lock blockingChannelLock = new ReentrantLock(); public boolean flush(int milliseconds) { if (blocking) { boolean locked = false; try { locked = bufferLock.tryLock(milliseconds,TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.interrupted(); } if (locked) { try { ((SocketChannel)key.channel()).socket().setSoTimeout(milliseconds); varOutBuffer.writeToChannel((SocketChannel)key.channel()); ((SocketChannel)key.channel()).socket().setSoTimeout(cp.getSocketTimeout()); } catch(IOException ioe) { logger.debug(ioe + toString(),ioe); } finally { bufferLock.unlock(); } } return varOutBuffer.hasRemaining(); } else { long sleepEnd = System.currentTimeMillis() + milliseconds; bufferLock.lock(); try { while (varOutBuffer.hasRemaining() && System.currentTimeMillis() < sleepEnd) { try { bytesChanged.await(20,TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.interrupted(); break; } } return varOutBuffer.hasRemaining(); } finally { bufferLock.unlock(); } } } @Override public InetSocketAddress getInetSocketAddress() { synchronized (inetAddySynch) { if (inetAddress == null || inetAddress.getAddress() == null) { return null; } return inetAddress; } } @Override public ByteChannel retrieveChannel() throws IOException { SocketChannel sc = (SocketChannel)key.channel(); MultiStandardConnection.get().synchExec(new Runnable() { public void run() { key.cancel(); } }); sc.configureBlocking(true); sc.socket().setSoTimeout(10000); blocking = true; return new BlockingChannel(); } @Override public boolean returnChannel(ByteChannel bc) { if (!blocking) { throw new IllegalStateException(); } SocketChannel soChan = (SocketChannel)key.channel(); if (soChan.isOpen()) { try { soChan.configureBlocking(false); } catch(IOException ioe) { return false; } blocking = false; MultiStandardConnection.get().register(soChan, this, true); return soChan.isOpen(); } else { close(); return false; } } @Override public void setIncomingDecompression(Compression comp) throws IOException { varInBuffer.setDecompression(comp,bufferLock,bytesChanged); } public boolean usesEncryption() { return encryption; } /** * @author Quicksilver * */ private class BlockingChannel implements ByteChannel { private BlockingChannel() {} public int write(ByteBuffer src) throws IOException { UnblockingConnection.this.send(src); bufferLock.lock(); try { int toWrite = varOutBuffer.writeToChannel((SocketChannel) key.channel()); return toWrite; } finally { bufferLock.unlock(); } } public int read(ByteBuffer dst) throws IOException { boolean varInBufferHasRemaining; bufferLock.lock(); try { varInBufferHasRemaining = varInBuffer.hasRemaining(); } finally { bufferLock.unlock(); } while (!varInBufferHasRemaining) { UnblockingConnection.this.read(); bufferLock.lock(); try { if (!encryption || engine.isInboundDone()) { //without encryption read is always successful... also we need a break if encryption is done.. break; } varInBufferHasRemaining = varInBuffer.hasRemaining(); } finally { bufferLock.unlock(); } } bufferLock.lock(); try { return varInBuffer.getBytes(dst); } finally { bufferLock.unlock(); } } public void close() throws IOException { UnblockingConnection.this.close(); } public boolean isOpen() { return key.channel().isOpen(); } } }