/*
* Copyright (c) 2012 the original author or authors.
*
* 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.eclipse.jetty.spdy.http;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.AsyncEndPoint;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.Buffers;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.nio.AsyncConnection;
import org.eclipse.jetty.io.nio.DirectNIOBuffer;
import org.eclipse.jetty.io.nio.IndirectNIOBuffer;
import org.eclipse.jetty.io.nio.NIOBuffer;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.spdy.SPDYAsyncConnection;
import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implements AsyncConnection
{
private static final Logger logger = LoggerFactory.getLogger(ServerHTTPSPDYAsyncConnection.class);
private static final ByteBuffer ZERO_BYTES = ByteBuffer.allocate(0);
private final Queue<Runnable> tasks = new LinkedList<>();
private final BlockingQueue<DataInfo> dataInfos = new LinkedBlockingQueue<>();
private final SPDYAsyncConnection connection;
private final Stream stream;
private Headers headers; // No need for volatile, guarded by state
private DataInfo dataInfo; // No need for volatile, guarded by state
private NIOBuffer buffer; // No need for volatile, guarded by state
private boolean complete; // No need for volatile, guarded by state
private volatile State state = State.INITIAL;
private boolean dispatched; // Guarded by synchronization on tasks
public ServerHTTPSPDYAsyncConnection(Connector connector, AsyncEndPoint endPoint, Server server, SPDYAsyncConnection connection, Stream stream)
{
super(connector, endPoint, server);
this.connection = connection;
this.stream = stream;
getParser().setPersistent(true);
}
@Override
protected HttpParser newHttpParser(Buffers requestBuffers, EndPoint endPoint, HttpParser.EventHandler requestHandler)
{
return new HTTPSPDYParser(requestBuffers, endPoint);
}
@Override
protected HttpGenerator newHttpGenerator(Buffers responseBuffers, EndPoint endPoint)
{
return new HTTPSPDYGenerator(responseBuffers, endPoint);
}
@Override
public AsyncEndPoint getEndPoint()
{
return (AsyncEndPoint)super.getEndPoint();
}
private void post(Runnable task)
{
synchronized (tasks)
{
logger.debug("Posting task {}", task);
tasks.offer(task);
dispatch();
}
}
private void dispatch()
{
synchronized (tasks)
{
if (dispatched)
return;
final Runnable task = tasks.poll();
if (task != null)
{
dispatched = true;
logger.debug("Dispatching task {}", task);
getServer().getThreadPool().dispatch(new Runnable()
{
@Override
public void run()
{
logger.debug("Executing task {}", task);
task.run();
logger.debug("Completing task {}", task);
dispatched = false;
dispatch();
}
});
}
}
}
@Override
public Connection handle()
{
setCurrentConnection(this);
try
{
switch (state)
{
case INITIAL:
{
break;
}
case REQUEST:
{
Headers.Header method = headers.get("method");
Headers.Header uri = headers.get("url");
Headers.Header version = headers.get("version");
if (method == null || uri == null || version == null)
throw new HttpException(HttpStatus.BAD_REQUEST_400);
String m = method.value();
String u = uri.value();
String v = version.value();
logger.debug("HTTP > {} {} {}", new Object[]{m, u, v});
startRequest(new ByteArrayBuffer(m), new ByteArrayBuffer(u), new ByteArrayBuffer(v));
state = State.HEADERS;
handle();
break;
}
case HEADERS:
{
for (Headers.Header header : headers)
{
String name = header.name();
switch (name)
{
case "method":
case "version":
case "url":
{
// Skip request line headers
continue;
}
case "connection":
case "keep-alive":
case "proxy-connection":
case "transfer-encoding":
{
// Spec says to ignore these headers
continue;
}
default:
{
// Spec says headers must be single valued
String value = header.value();
logger.debug("HTTP > {}: {}", name, value);
parsedHeader(new ByteArrayBuffer(name), new ByteArrayBuffer(value));
break;
}
}
}
break;
}
case HEADERS_COMPLETE:
{
headerComplete();
break;
}
case CONTENT:
{
final Buffer buffer = this.buffer;
if (buffer != null && buffer.length() > 0)
content(buffer);
break;
}
case FINAL:
{
messageComplete(0);
break;
}
case ASYNC:
{
handleRequest();
break;
}
default:
{
throw new IllegalStateException();
}
}
return this;
}
catch (HttpException x)
{
respond(stream, x.getStatus());
return this;
}
catch (IOException x)
{
close(stream);
return this;
}
finally
{
setCurrentConnection(null);
}
}
private void respond(Stream stream, int status)
{
Headers headers = new Headers();
headers.put("status", String.valueOf(status));
headers.put("version", "HTTP/1.1");
stream.reply(new ReplyInfo(headers, true));
}
private void close(Stream stream)
{
stream.getSession().goAway();
}
@Override
public void onInputShutdown() throws IOException
{
}
public void beginRequest(final Headers headers)
{
this.headers = headers.isEmpty() ? null : headers;
post(new Runnable()
{
@Override
public void run()
{
if (!headers.isEmpty())
state = State.REQUEST;
handle();
}
});
}
public void headers(Headers headers)
{
this.headers = headers;
post(new Runnable()
{
@Override
public void run()
{
state = state == State.INITIAL ? State.REQUEST : State.HEADERS;
handle();
}
});
}
public void content(final DataInfo dataInfo, boolean endRequest)
{
dataInfos.offer(new ByteBufferDataInfo(dataInfo.asByteBuffer(false), dataInfo.isClose(), dataInfo.isCompress())
{
@Override
public void consume(int delta)
{
super.consume(delta);
dataInfo.consume(delta);
}
});
complete = endRequest;
post(new Runnable()
{
@Override
public void run()
{
logger.debug("HTTP > {} bytes of content", dataInfo.length());
if (state == State.HEADERS)
{
state = State.HEADERS_COMPLETE;
handle();
}
state = State.CONTENT;
handle();
}
});
}
public void endRequest()
{
post(new Runnable()
{
public void run()
{
if (state == State.HEADERS)
{
state = State.HEADERS_COMPLETE;
handle();
}
state = State.FINAL;
handle();
}
});
}
public void async()
{
post(new Runnable()
{
@Override
public void run()
{
State currentState = state;
state = State.ASYNC;
handle();
state = currentState;
}
});
}
private Buffer consumeContent(long maxIdleTime) throws IOException, InterruptedException
{
while (true)
{
// Volatile read to ensure visibility
State state = this.state;
if (state != State.HEADERS_COMPLETE && state != State.CONTENT && state != State.FINAL)
throw new IllegalStateException();
if (buffer != null)
{
if (buffer.length() > 0)
{
logger.debug("Consuming content bytes, {} available", buffer.length());
return buffer;
}
else
{
// The application has consumed the buffer, so consume also the DataInfo
if (dataInfo.consumed() == 0)
dataInfo.consume(dataInfo.length());
dataInfo = null;
buffer = null;
if (complete && dataInfos.isEmpty())
return null;
// Loop to get content bytes from DataInfos
}
}
else
{
logger.debug("Waiting at most {} ms for content bytes", maxIdleTime);
long begin = System.nanoTime();
dataInfo = dataInfos.poll(maxIdleTime, TimeUnit.MILLISECONDS);
long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin);
logger.debug("Waited {} ms for content bytes", elapsed);
if (dataInfo != null)
{
// Only consume if it's the last DataInfo
boolean consume = complete && dataInfos.isEmpty();
ByteBuffer byteBuffer = dataInfo.asByteBuffer(consume);
buffer = byteBuffer.isDirect() ? new DirectNIOBuffer(byteBuffer, false) : new IndirectNIOBuffer(byteBuffer, false);
// Loop to return the buffer
}
else
{
stream.getSession().goAway();
throw new EOFException("read timeout");
}
}
}
}
private int availableContent()
{
// Volatile read to ensure visibility
State state = this.state;
if (state != State.HEADERS_COMPLETE && state != State.CONTENT)
throw new IllegalStateException();
return buffer == null ? 0 : buffer.length();
}
@Override
public void commitResponse(boolean last) throws IOException
{
// Keep the original behavior since it just delegates to the generator
super.commitResponse(last);
}
@Override
public void flushResponse() throws IOException
{
// Just commit the response, if necessary: flushing buffers will be taken care of in complete()
commitResponse(false);
}
@Override
public void completeResponse() throws IOException
{
// Keep the original behavior since it just delegates to the generator
super.completeResponse();
}
private enum State
{
INITIAL, REQUEST, HEADERS, HEADERS_COMPLETE, CONTENT, FINAL, ASYNC
}
/**
* Needed in order to override parser methods that read content.
*/
private class HTTPSPDYParser extends HttpParser
{
public HTTPSPDYParser(Buffers buffers, EndPoint endPoint)
{
super(buffers, endPoint, new HTTPSPDYParserHandler());
}
@Override
public Buffer blockForContent(long maxIdleTime) throws IOException
{
try
{
return consumeContent(maxIdleTime);
}
catch (InterruptedException x)
{
throw new InterruptedIOException();
}
}
@Override
public int available() throws IOException
{
return availableContent();
}
}
/**
* Empty implementation, since it won't parse anything
*/
private static class HTTPSPDYParserHandler extends HttpParser.EventHandler
{
@Override
public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException
{
}
@Override
public void content(Buffer ref) throws IOException
{
}
@Override
public void startResponse(Buffer version, int status, Buffer reason) throws IOException
{
}
}
/**
* Needed in order to override generator methods that would generate HTTP,
* since we must generate SPDY instead.
*/
private class HTTPSPDYGenerator extends HttpGenerator
{
private boolean closed;
private HTTPSPDYGenerator(Buffers buffers, EndPoint endPoint)
{
super(buffers, endPoint);
}
@Override
public void send1xx(int code) throws IOException
{
// TODO: not supported yet, but unlikely to be called
throw new UnsupportedOperationException();
}
@Override
public void sendResponse(Buffer response) throws IOException
{
// Do not think this method is ever used.
// Jetty calls it from Request.setAttribute() only if the attribute
// "org.eclipse.jetty.server.ResponseBuffer", seems like a hack.
throw new UnsupportedOperationException();
}
@Override
public void sendError(int code, String reason, String content, boolean close) throws IOException
{
// Keep original behavior because it's delegating to other methods that we override.
super.sendError(code, reason, content, close);
}
@Override
public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException
{
Headers headers = new Headers();
String version = "HTTP/1.1";
headers.put("version", version);
StringBuilder status = new StringBuilder().append(_status);
if (_reason != null)
status.append(" ").append(_reason.toString("UTF-8"));
headers.put("status", status.toString());
logger.debug("HTTP < {} {}", version, status);
if (fields != null)
{
for (int i = 0; i < fields.size(); ++i)
{
HttpFields.Field field = fields.getField(i);
String name = field.getName().toLowerCase();
String value = field.getValue();
headers.put(name, value);
logger.debug("HTTP < {}: {}", name, value);
}
}
// We have to query the HttpGenerator and its buffers to know
// whether there is content buffered; if so, send the data frame
Buffer content = getContentBuffer();
stream.reply(new ReplyInfo(headers, content == null));
if (content != null)
{
closed = allContentAdded || isAllContentWritten();
ByteBuffer buffer = ByteBuffer.wrap(content.asArray());
logger.debug("HTTP < {} bytes of content", buffer.remaining());
// Send the data frame
stream.data(new ByteBufferDataInfo(buffer, closed));
// Update HttpGenerator fields so that they remain consistent
content.clear();
_state = closed ? HttpGenerator.STATE_END : HttpGenerator.STATE_CONTENT;
}
else
{
closed = true;
// Update HttpGenerator fields so that they remain consistent
_state = HttpGenerator.STATE_END;
}
}
private Buffer getContentBuffer()
{
if (_buffer != null && _buffer.length() > 0)
return _buffer;
if (_bypass && _content != null && _content.length() > 0)
return _content;
return null;
}
@Override
public boolean addContent(byte b) throws IOException
{
// In HttpGenerator, writing one byte only has a different path than
// writing a buffer. Here we normalize these path to keep it simpler.
addContent(new ByteArrayBuffer(new byte[]{b}), false);
return false;
}
@Override
public void addContent(Buffer content, boolean last) throws IOException
{
// Keep the original behavior since adding content will
// just accumulate bytes until the response is committed.
super.addContent(content, last);
}
@Override
public void flush(long maxIdleTime) throws IOException
{
while (_content != null && _content.length() > 0)
{
_content.skip(_buffer.put(_content));
ByteBuffer buffer = ByteBuffer.wrap(_buffer.asArray());
logger.debug("HTTP < {} bytes of content", buffer.remaining());
_buffer.clear();
closed = _content.length() == 0 && _last;
stream.data(new ByteBufferDataInfo(buffer, closed));
boolean expired = !connection.getEndPoint().blockWritable(maxIdleTime);
if (expired)
{
stream.getSession().goAway();
throw new EOFException("write timeout");
}
}
}
@Override
public int flushBuffer() throws IOException
{
// Must never be called because it's where the HttpGenerator writes
// the HTTP content to the EndPoint (we should write SPDY instead).
// If it's called it's our bug.
throw new UnsupportedOperationException();
}
@Override
public void blockForOutput(long maxIdleTime) throws IOException
{
// The semantic of this method is weird: not only it has to block
// but also need to flush. Since we have a blocking flush method
// we delegate to that, because it has the same semantic.
flush(maxIdleTime);
}
@Override
public void complete() throws IOException
{
Buffer content = getContentBuffer();
if (content != null)
{
ByteBuffer buffer = ByteBuffer.wrap(content.asArray());
logger.debug("HTTP < {} bytes of content", buffer.remaining());
// Update HttpGenerator fields so that they remain consistent
content.clear();
_state = STATE_END;
// Send the data frame
stream.data(new ByteBufferDataInfo(buffer, true));
}
else if (!closed)
{
closed = true;
_state = STATE_END;
// Send the data frame
stream.data(new ByteBufferDataInfo(ZERO_BYTES, true));
}
}
}
}