// ========================================================================
// Copyright 2004-2008 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed 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 org.mortbay.jetty.security;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import org.mortbay.io.Buffer;
import org.mortbay.io.Buffers;
import org.mortbay.io.nio.NIOBuffer;
import org.mortbay.io.nio.SelectChannelEndPoint;
import org.mortbay.io.nio.SelectorManager;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.log.Log;
/* ------------------------------------------------------------ */
/**
* SslHttpChannelEndPoint.
*
* @author Nik Gonzalez <ngonzalez@exist.com>
* @author Greg Wilkins <gregw@mortbay.com>
*/
public class SslHttpChannelEndPoint extends SelectChannelConnector.ConnectorEndPoint implements Runnable
{
private static final ByteBuffer[] __NO_BUFFERS={};
private Buffers _buffers;
private SSLEngine _engine;
private ByteBuffer _inBuffer;
private NIOBuffer _inNIOBuffer;
private ByteBuffer _outBuffer;
private NIOBuffer _outNIOBuffer;
private NIOBuffer[] _reuseBuffer=new NIOBuffer[2];
private ByteBuffer[] _gather=new ByteBuffer[2];
private boolean _closing=false;
private SSLEngineResult _result;
private String _last;
// ssl
protected SSLSession _session;
// TODO get rid of this
// StringBuilder h = new StringBuilder(500);
/* ------------------------------------------------------------ */
public SslHttpChannelEndPoint(Buffers buffers,SocketChannel channel, SelectorManager.SelectSet selectSet, SelectionKey key, SSLEngine engine)
throws SSLException, IOException
{
super(channel,selectSet,key);
_buffers=buffers;
// ssl
_engine=engine;
_session=engine.getSession();
// TODO pool buffers and use only when needed.
_outNIOBuffer=(NIOBuffer)buffers.getBuffer(_session.getPacketBufferSize());
_outBuffer=_outNIOBuffer.getByteBuffer();
_inNIOBuffer=(NIOBuffer)buffers.getBuffer(_session.getPacketBufferSize());
_inBuffer=_inNIOBuffer.getByteBuffer();
// h.append("CONSTRUCTED\n");
}
// TODO get rid of these dumps
public void dump()
{
System.err.println(_result);
// System.err.println(h.toString());
// System.err.println("--");
}
/* ------------------------------------------------------------ */
/* (non-Javadoc)
* @see org.mortbay.io.nio.SelectChannelEndPoint#idleExpired()
*/
protected void idleExpired()
{
try
{
_selectSet.getManager().dispatch(new Runnable()
{
public void run()
{
doIdleExpired();
}
});
}
catch(Exception e)
{
Log.ignore(e);
}
}
/* ------------------------------------------------------------ */
protected void doIdleExpired()
{
// h.append("IDLE EXPIRED\n");
super.idleExpired();
}
/* ------------------------------------------------------------ */
public void close() throws IOException
{
// TODO - this really should not be done in a loop here - but with async callbacks.
// h.append("CLOSE\n");
_closing=true;
try
{
int tries=0;
while (_outNIOBuffer.length()>0)
{
// TODO REMOVE loop check
if (tries++>100)
throw new IllegalStateException();
flush();
Thread.sleep(100); // TODO yuck
}
_engine.closeOutbound();
loop: while (isOpen() && !(_engine.isInboundDone() && _engine.isOutboundDone()))
{
// TODO REMOVE loop check
if (tries++>100)
throw new IllegalStateException();
if (_outNIOBuffer.length()>0)
{
flush();
Thread.sleep(100); // TODO yuck
}
switch(_engine.getHandshakeStatus())
{
case FINISHED:
case NOT_HANDSHAKING:
break loop;
case NEED_UNWRAP:
Buffer buffer =_buffers.getBuffer(_engine.getSession().getApplicationBufferSize());
try
{
ByteBuffer bbuffer = ((NIOBuffer)buffer).getByteBuffer();
if (!unwrap(bbuffer) && _engine.getHandshakeStatus()==HandshakeStatus.NEED_UNWRAP)
{
// h.append("break loop\n");
break loop;
}
}
catch(SSLException e)
{
Log.ignore(e);
}
finally
{
_buffers.returnBuffer(buffer);
}
break;
case NEED_TASK:
{
Runnable task;
while ((task=_engine.getDelegatedTask())!=null)
{
task.run();
}
break;
}
case NEED_WRAP:
{
if (_outNIOBuffer.length()>0)
flush();
try
{
_outNIOBuffer.compact();
int put=_outNIOBuffer.putIndex();
_outBuffer.position(put);
_result=null;
_last="close wrap";
_result=_engine.wrap(__NO_BUFFERS,_outBuffer);
_outNIOBuffer.setPutIndex(put+_result.bytesProduced());
}
finally
{
_outBuffer.position(0);
}
flush();
break;
}
}
}
}
catch(IOException e)
{
Log.ignore(e);
}
catch (InterruptedException e)
{
Log.ignore(e);
}
finally
{
super.close();
if (_inNIOBuffer!=null)
_buffers.returnBuffer(_inNIOBuffer);
if (_outNIOBuffer!=null)
_buffers.returnBuffer(_outNIOBuffer);
if (_reuseBuffer[0]!=null)
_buffers.returnBuffer(_reuseBuffer[0]);
if (_reuseBuffer[1]!=null)
_buffers.returnBuffer(_reuseBuffer[1]);
}
}
/* ------------------------------------------------------------ */
/*
*/
public int fill(Buffer buffer) throws IOException
{
ByteBuffer bbuf=extractInputBuffer(buffer);
int size=buffer.length();
HandshakeStatus initialStatus = _engine.getHandshakeStatus();
synchronized (bbuf)
{
try
{
unwrap(bbuf);
int tries=0, wraps=0;
loop: while (true)
{
// TODO REMOVE loop check
if (tries++>100)
throw new IllegalStateException();
// h.append("Fill(Buffer)\n");
if (_outNIOBuffer.length()>0)
flush();
// h.append("status=").append(_engine.getHandshakeStatus()).append('\n');
switch(_engine.getHandshakeStatus())
{
case FINISHED:
case NOT_HANDSHAKING:
if (_closing)
return -1;
break loop;
case NEED_UNWRAP:
if (!unwrap(bbuf) && _engine.getHandshakeStatus()==HandshakeStatus.NEED_UNWRAP)
{
// h.append("break loop\n");
break loop;
}
break;
case NEED_TASK:
{
Runnable task;
while ((task=_engine.getDelegatedTask())!=null)
{
// h.append("run task\n");
task.run();
}
if(initialStatus==HandshakeStatus.NOT_HANDSHAKING &&
HandshakeStatus.NEED_UNWRAP==_engine.getHandshakeStatus() && wraps==0)
{
// java sslengine bug on TLS.. this should be NEED_WRAP
// because a handshake response is needed to be sent to the client
return -1;
}
break;
}
case NEED_WRAP:
{
wraps++;
synchronized(_outBuffer)
{
try
{
_outNIOBuffer.compact();
int put=_outNIOBuffer.putIndex();
_outBuffer.position();
_result=null;
_last="fill wrap";
_result=_engine.wrap(__NO_BUFFERS,_outBuffer);
switch(_result.getStatus())
{
case BUFFER_OVERFLOW:
case BUFFER_UNDERFLOW:
Log.warn("wrap {}",_result);
case CLOSED:
_closing=true;
}
// h.append("wrap ").append(result).append('\n');
_outNIOBuffer.setPutIndex(put+_result.bytesProduced());
}
finally
{
_outBuffer.position(0);
}
}
flush();
break;
}
}
}
}
catch(SSLException e)
{
Log.warn(e.toString());
Log.debug(e);
throw e;
}
finally
{
buffer.setPutIndex(bbuf.position());
bbuf.position(0);
}
}
return buffer.length()-size;
}
/* ------------------------------------------------------------ */
public int flush(Buffer buffer) throws IOException
{
return flush(buffer,null,null);
}
/* ------------------------------------------------------------ */
/*
*/
public int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException
{
int consumed=0;
int available=header.length();
if (buffer!=null)
available+=buffer.length();
int tries=0;
loop: while (true)
{
// TODO REMOVE loop check
if (tries++>100)
throw new IllegalStateException();
// h.append("Flush ").append(tries).append(' ').append(_outNIOBuffer.length()).append('\n');
if (_outNIOBuffer.length()>0)
flush();
// h.append(_engine.getHandshakeStatus()).append('\n');
switch(_engine.getHandshakeStatus())
{
case FINISHED:
case NOT_HANDSHAKING:
if (_closing || available==0)
{
if (consumed==0)
consumed= -1;
break loop;
}
int c=(header!=null && buffer!=null)?wrap(header,buffer):wrap(header);
if (c>0)
{
consumed+=c;
available-=c;
}
else if (c<0)
{
if (consumed==0)
consumed=-1;
break loop;
}
break;
case NEED_UNWRAP:
Buffer buf =_buffers.getBuffer(_engine.getSession().getApplicationBufferSize());
try
{
ByteBuffer bbuf = ((NIOBuffer)buf).getByteBuffer();
if (!unwrap(bbuf) && _engine.getHandshakeStatus()==HandshakeStatus.NEED_UNWRAP)
{
// h.append("break").append('\n');
break loop;
}
}
finally
{
_buffers.returnBuffer(buf);
}
break;
case NEED_TASK:
{
Runnable task;
while ((task=_engine.getDelegatedTask())!=null)
{
// h.append("run task\n");
task.run();
}
break;
}
case NEED_WRAP:
{
synchronized(_outBuffer)
{
try
{
_outNIOBuffer.compact();
int put=_outNIOBuffer.putIndex();
_outBuffer.position();
_result=null;
_last="flush wrap";
_result=_engine.wrap(__NO_BUFFERS,_outBuffer);
switch(_result.getStatus())
{
case BUFFER_OVERFLOW:
case BUFFER_UNDERFLOW:
Log.warn("wrap {}",_result);
case CLOSED:
_closing=true;
}
// h.append("wrap=").append(result).append('\n');
_outNIOBuffer.setPutIndex(put+_result.bytesProduced());
}
finally
{
_outBuffer.position(0);
}
}
flush();
break;
}
}
}
return consumed;
}
/* ------------------------------------------------------------ */
public void flush() throws IOException
{
while (_outNIOBuffer.length()>0)
{
int flushed=super.flush(_outNIOBuffer);
// h.append("flushed=").append(flushed).append(" of ").append(_outNIOBuffer.length()).append('\n');
if (flushed==0)
{
Thread.yield();
flushed=super.flush(_outNIOBuffer);
// h.append("flushed2=").append(flushed).append(" of ").append(_outNIOBuffer.length()).append('\n');
}
}
}
/* ------------------------------------------------------------ */
private ByteBuffer extractInputBuffer(Buffer buffer)
{
assert buffer instanceof NIOBuffer;
NIOBuffer nbuf=(NIOBuffer)buffer;
ByteBuffer bbuf=nbuf.getByteBuffer();
bbuf.position(buffer.putIndex());
return bbuf;
}
/* ------------------------------------------------------------ */
/**
* @return true if progress is made
*/
private boolean unwrap(ByteBuffer buffer) throws IOException
{
if (_inNIOBuffer.hasContent())
_inNIOBuffer.compact();
else
_inNIOBuffer.clear();
int total_filled=0;
while (_inNIOBuffer.space()>0 && super.isOpen())
{
try
{
int filled=super.fill(_inNIOBuffer);
// h.append("fill=").append(filled).append('\n');
if (filled<=0)
break;
total_filled+=filled;
}
catch(IOException e)
{
if (_inNIOBuffer.length()==0)
throw e;
break;
}
}
// h.append("inNIOBuffer=").append(_inNIOBuffer.length()).append('\n');
if (_inNIOBuffer.length()==0)
{
if(!isOpen())
throw new org.mortbay.jetty.EofException();
return false;
}
try
{
_inBuffer.position(_inNIOBuffer.getIndex());
_inBuffer.limit(_inNIOBuffer.putIndex());
_result=null;
_last="unwrap";
_result=_engine.unwrap(_inBuffer,buffer);
// h.append("unwrap=").append(result).append('\n');
_inNIOBuffer.skip(_result.bytesConsumed());
}
finally
{
_inBuffer.position(0);
_inBuffer.limit(_inBuffer.capacity());
}
switch(_result.getStatus())
{
case BUFFER_OVERFLOW:
case BUFFER_UNDERFLOW:
if (Log.isDebugEnabled()) Log.debug("unwrap {}",_result);
return (total_filled > 0);
case CLOSED:
_closing=true;
case OK:
boolean progress=total_filled>0 ||_result.bytesConsumed()>0 || _result.bytesProduced()>0;
// h.append("progress=").append(progress).append('\n');
return progress;
default:
Log.warn("unwrap "+_result);
throw new IOException(_result.toString());
}
}
/* ------------------------------------------------------------ */
private ByteBuffer extractOutputBuffer(Buffer buffer,int n)
{
NIOBuffer nBuf=null;
if (buffer.buffer() instanceof NIOBuffer)
{
nBuf=(NIOBuffer)buffer.buffer();
return nBuf.getByteBuffer();
}
else
{
if (_reuseBuffer[n]==null)
_reuseBuffer[n] = (NIOBuffer)_buffers.getBuffer(_session.getApplicationBufferSize());
NIOBuffer buf = _reuseBuffer[n];
buf.clear();
buf.put(buffer);
return buf.getByteBuffer();
}
}
/* ------------------------------------------------------------ */
private int wrap(Buffer header, Buffer buffer) throws IOException
{
_gather[0]=extractOutputBuffer(header,0);
synchronized(_gather[0])
{
_gather[0].position(header.getIndex());
_gather[0].limit(header.putIndex());
_gather[1]=extractOutputBuffer(buffer,1);
synchronized(_gather[1])
{
_gather[1].position(buffer.getIndex());
_gather[1].limit(buffer.putIndex());
synchronized(_outBuffer)
{
int consumed=0;
try
{
_outNIOBuffer.clear();
_outBuffer.position(0);
_outBuffer.limit(_outBuffer.capacity());
_result=null;
_last="wrap wrap";
_result=_engine.wrap(_gather,_outBuffer);
// h.append("wrap2=").append(result).append('\n');
_outNIOBuffer.setGetIndex(0);
_outNIOBuffer.setPutIndex(_result.bytesProduced());
consumed=_result.bytesConsumed();
}
finally
{
_outBuffer.position(0);
if (consumed>0 && header!=null)
{
int len=consumed<header.length()?consumed:header.length();
header.skip(len);
consumed-=len;
_gather[0].position(0);
_gather[0].limit(_gather[0].capacity());
}
if (consumed>0 && buffer!=null)
{
int len=consumed<buffer.length()?consumed:buffer.length();
buffer.skip(len);
consumed-=len;
_gather[1].position(0);
_gather[1].limit(_gather[1].capacity());
}
assert consumed==0;
}
}
}
}
switch(_result.getStatus())
{
case BUFFER_OVERFLOW:
case BUFFER_UNDERFLOW:
Log.warn("wrap {}",_result);
case OK:
return _result.bytesConsumed();
case CLOSED:
_closing=true;
return _result.bytesConsumed()>0?_result.bytesConsumed():-1;
default:
Log.warn("wrap "+_result);
throw new IOException(_result.toString());
}
}
/* ------------------------------------------------------------ */
private int wrap(Buffer header) throws IOException
{
_gather[0]=extractOutputBuffer(header,0);
synchronized(_gather[0])
{
_gather[0].position(header.getIndex());
_gather[0].limit(header.putIndex());
int consumed=0;
synchronized(_outBuffer)
{
try
{
_outNIOBuffer.clear();
_outBuffer.position(0);
_outBuffer.limit(_outBuffer.capacity());
_result=null;
_last="wrap wrap";
_result=_engine.wrap(_gather[0],_outBuffer);
// h.append("wrap1=").append(result).append('\n');
_outNIOBuffer.setGetIndex(0);
_outNIOBuffer.setPutIndex(_result.bytesProduced());
consumed=_result.bytesConsumed();
}
finally
{
_outBuffer.position(0);
if (consumed>0 && header!=null)
{
int len=consumed<header.length()?consumed:header.length();
header.skip(len);
consumed-=len;
_gather[0].position(0);
_gather[0].limit(_gather[0].capacity());
}
assert consumed==0;
}
}
}
switch(_result.getStatus())
{
case BUFFER_OVERFLOW:
case BUFFER_UNDERFLOW:
Log.warn("wrap {}",_result);
case OK:
return _result.bytesConsumed();
case CLOSED:
_closing=true;
return _result.bytesConsumed()>0?_result.bytesConsumed():-1;
default:
Log.warn("wrap "+_result);
throw new IOException(_result.toString());
}
}
/* ------------------------------------------------------------ */
public boolean isBufferingInput()
{
return _inNIOBuffer.hasContent();
}
/* ------------------------------------------------------------ */
public boolean isBufferingOutput()
{
return _outNIOBuffer.hasContent();
}
/* ------------------------------------------------------------ */
public boolean isBufferred()
{
return true;
}
/* ------------------------------------------------------------ */
public SSLEngine getSSLEngine()
{
return _engine;
}
/* ------------------------------------------------------------ */
public String toString()
{
return super.toString()+","+_engine.getHandshakeStatus()+", in/out="+_inNIOBuffer.length()+"/"+_outNIOBuffer.length()+" last "+_last+" "+_result;
}
}