package org.glassfish.grizzly.http2;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.SSLEngine;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.EmptyCompletionHandler;
import org.glassfish.grizzly.IOEvent;
import org.glassfish.grizzly.filterchain.FilterChain;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.filterchain.FilterChainContext.TransportContext;
import org.glassfish.grizzly.filterchain.FilterChainEvent;
import org.glassfish.grizzly.filterchain.NextAction;
import org.glassfish.grizzly.http.HttpContent;
import org.glassfish.grizzly.http.HttpContext;
import org.glassfish.grizzly.http.HttpEvents;
import org.glassfish.grizzly.http.HttpEvents.OutgoingHttpUpgradeEvent;
import org.glassfish.grizzly.http.HttpHeader;
import org.glassfish.grizzly.http.HttpPacket;
import org.glassfish.grizzly.http.HttpRequestPacket;
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.HttpTrailer;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.Protocol;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HeaderValue;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.grizzly.http.util.MimeHeaders;
import org.glassfish.grizzly.http2.NetLogger.Context;
import org.glassfish.grizzly.http2.frames.ErrorCode;
import org.glassfish.grizzly.http2.frames.HeaderBlockHead;
import org.glassfish.grizzly.http2.frames.Http2Frame;
import org.glassfish.grizzly.http2.frames.PushPromiseFrame;
import org.glassfish.grizzly.http2.frames.HeadersFrame;
import org.glassfish.grizzly.http2.frames.SettingsFrame;
import org.glassfish.grizzly.memory.Buffers;
import org.glassfish.grizzly.npn.AlpnClientNegotiator;
import org.glassfish.grizzly.ssl.SSLFilter;
import static org.glassfish.grizzly.http2.Termination.IN_FIN_TERMINATION;
import static org.glassfish.grizzly.http2.Termination.OUT_FIN_TERMINATION;
import static org.glassfish.grizzly.http2.frames.SettingsFrame.SETTINGS_ENABLE_PUSH;
import static org.glassfish.grizzly.http2.frames.SettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE;
import static org.glassfish.grizzly.http2.frames.SettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS;
public class Http2ClientFilter extends Http2BaseFilter {
private final AlpnClientNegotiatorImpl defaultClientAlpnNegotiator;
private boolean isNeverForceUpgrade;
private boolean sendPushRequestUpstream;
private final HeaderValue defaultHttp2Upgrade;
private final HeaderValue ;
public Http2ClientFilter(final Http2Configuration configuration) {
super(configuration);
defaultClientAlpnNegotiator =
new AlpnClientNegotiatorImpl(this);
defaultHttp2Upgrade = HeaderValue.newHeaderValue(HTTP2_CLEAR);
connectionUpgradeHeaderValue =
HeaderValue.newHeaderValue("Upgrade, HTTP2-Settings");
}
@SuppressWarnings("unused")
public boolean isNeverForceUpgrade() {
return isNeverForceUpgrade;
}
@SuppressWarnings("unused")
public void setNeverForceUpgrade(boolean neverForceUpgrade) {
this.isNeverForceUpgrade = neverForceUpgrade;
}
@SuppressWarnings("unused")
public boolean isSendPushRequestUpstream() {
return sendPushRequestUpstream;
}
@SuppressWarnings("unused")
public void setSendPushRequestUpstream(boolean sendPushRequestUpstream) {
this.sendPushRequestUpstream = sendPushRequestUpstream;
}
@Override
public NextAction handleConnect(final FilterChainContext ctx) throws IOException {
final Connection connection = ctx.getConnection();
final FilterChain filterChain = (FilterChain) connection.getProcessor();
final int idx = filterChain.indexOfType(SSLFilter.class);
if (idx != -1) {
final SSLFilter sslFilter = (SSLFilter) filterChain.get(idx);
AlpnSupport.getInstance().configure(sslFilter);
AlpnSupport.getInstance().setClientSideNegotiator(
connection, getClientAlpnNegotiator());
final NextAction suspendAction = ctx.getSuspendAction();
ctx.suspend();
sslFilter.handshake(connection, new EmptyCompletionHandler<SSLEngine>() {
@Override
public void completed(final SSLEngine result) {
ctx.resumeNext();
}
@Override
public void failed(Throwable throwable) {
ctx.fail(throwable);
}
});
connection.enableIOEvent(IOEvent.READ);
return suspendAction;
} else if (getConfiguration().isPriorKnowledge()) {
final Http2Session http2Session = createClientHttp2Session(connection);
final Http2State state = http2Session.getHttp2State();
state.setDirectUpgradePhase();
http2Session.sendPreface();
final NextAction suspendAction = ctx.getSuspendAction();
ctx.suspend();
state.addReadyListener(new Http2State.ReadyListener() {
@Override
public void ready(Http2Session http2Session) {
state.onClientHttpUpgradeRequestFinished();
http2Session.setupFilterChains(ctx, true);
ctx.resumeNext();
}
});
connection.enableIOEvent(IOEvent.READ);
return suspendAction;
}
return ctx.getInvokeAction();
}
@SuppressWarnings("unchecked")
@Override
public NextAction handleRead(final FilterChainContext ctx)
throws IOException {
if (checkIfHttp2StreamChain(ctx)) {
return ctx.getInvokeAction();
}
final Connection connection = ctx.getConnection();
Http2State http2State = Http2State.get(connection);
if (http2State == null || http2State.isNeverHttp2()) {
return ctx.getInvokeAction();
}
final HttpContent httpContent = ctx.getMessage();
final HttpHeader httpHeader = httpContent.getHttpHeader();
if (http2State.isHttpUpgradePhase()) {
assert !httpHeader.isRequest();
final HttpResponsePacket httpResponse = (HttpResponsePacket) httpHeader;
final HttpRequestPacket httpRequest = httpResponse.getRequest();
if (!tryHttpUpgrade(ctx, http2State, httpRequest, httpResponse)) {
http2State.setNeverHttp2();
return ctx.getInvokeAction();
}
}
final Http2Session http2Session =
obtainHttp2Session(http2State, ctx, true);
final Buffer framePayload = httpContent.getContent();
httpContent.recycle();
final List<Http2Frame> framesList =
frameCodec.parse(http2Session,
http2State.getFrameParsingState(),
framePayload);
if (!processFrames(ctx, http2Session, framesList)) {
return ctx.getSuspendAction();
}
return ctx.getStopAction();
}
@Override
public NextAction handleWrite(final FilterChainContext ctx)
throws IOException {
final Connection connection = ctx.getConnection();
Http2State http2State = Http2State.get(connection);
if (http2State != null && http2State.isNeverHttp2()) {
return ctx.getInvokeAction();
}
if (http2State == null) {
http2State = Http2State.create(connection);
final Object msg = ctx.getMessage();
if (!tryInsertHttpUpgradeHeaders(connection, msg)) {
http2State.setNeverHttp2();
}
assert HttpPacket.isHttp(ctx.getMessage());
checkIfLastHttp11Chunk(ctx, http2State, msg);
return ctx.getInvokeAction();
} else {
if (http2State.isHttpUpgradePhase()) {
final Object msg = ctx.getMessage();
if (HttpPacket.isHttp(msg)) {
if (!((HttpPacket) msg).getHttpHeader().isCommitted()) {
throw new IllegalStateException("Can't pipeline HTTP requests because it's still not clear if HTTP/1.x or HTTP/2 will be used");
}
checkIfLastHttp11Chunk(ctx, http2State, msg);
}
return ctx.getInvokeAction();
}
}
return super.handleWrite(ctx);
}
@Override
@SuppressWarnings("unchecked")
public NextAction handleEvent(final FilterChainContext ctx,
final FilterChainEvent event) throws IOException {
if (!Http2State.isHttp2(ctx.getConnection())) {
return ctx.getInvokeAction();
}
final Object type = event.type();
if (type == OutgoingHttpUpgradeEvent.TYPE) {
assert event instanceof OutgoingHttpUpgradeEvent;
final OutgoingHttpUpgradeEvent outUpgradeEvent =
(OutgoingHttpUpgradeEvent) event;
outUpgradeEvent.getHttpHeader().setIgnoreContentModifiers(false);
return ctx.getStopAction();
}
return ctx.getInvokeAction();
}
@SuppressWarnings("unchecked")
@Override
protected void (final FilterChainContext ctx,
final Http2Session http2Session,
final HttpHeader httpHeader,
final HttpPacket entireHttpPacket) throws IOException {
if (!http2Session.isHttp2OutputEnabled()) {
return;
}
final HttpRequestPacket request = (HttpRequestPacket) httpHeader;
if (!request.isCommitted()) {
prepareOutgoingRequest(request);
}
final Http2Stream stream = Http2Stream.getStreamFor(request);
if (stream == null) {
processOutgoingRequestForNewStream(ctx, http2Session, request,
entireHttpPacket);
} else {
final TransportContext transportContext = ctx.getTransportContext();
stream.getOutputSink().writeDownStream(entireHttpPacket,
ctx,
transportContext.getCompletionHandler(),
transportContext.getMessageCloner());
}
}
@SuppressWarnings("unchecked")
private void processOutgoingRequestForNewStream(final FilterChainContext ctx,
final Http2Session http2Session,
final HttpRequestPacket request,
final HttpPacket entireHttpPacket) throws IOException {
final ReentrantLock newStreamLock = http2Session.getNewClientStreamLock();
newStreamLock.lock();
try {
final Http2Stream stream = http2Session.openStream(
request,
http2Session.getNextLocalStreamId(),
0, false, 0);
if (stream == null) {
throw new IOException("Http2Session is closed");
}
request.setAttribute(Http2Stream.HTTP2_STREAM_ATTRIBUTE, stream);
final TransportContext transportContext = ctx.getTransportContext();
stream.getOutputSink().writeDownStream(entireHttpPacket,
ctx,
transportContext.getCompletionHandler(),
transportContext.getMessageCloner());
} finally {
newStreamLock.unlock();
}
}
protected Http2Session createClientHttp2Session(final Connection connection) {
return createHttp2Session(connection, false);
}
protected AlpnClientNegotiator getClientAlpnNegotiator() {
return defaultClientAlpnNegotiator;
}
@SuppressWarnings("DuplicateThrows")
private boolean tryHttpUpgrade(final FilterChainContext ctx,
final Http2State http2State,
final HttpRequestPacket httpRequest,
final HttpResponsePacket httpResponse)
throws Http2StreamException, IOException {
if (httpRequest == null) {
return false;
}
if (!checkRequestHeadersOnUpgrade(httpRequest)) {
return false;
}
if (!checkResponseHeadersOnUpgrade(httpResponse)) {
return false;
}
final Connection connection = ctx.getConnection();
http2State.setDirectUpgradePhase();
final Http2Session http2Session =
createClientHttp2Session(connection);
if (http2State.tryLockClientPreface()) {
http2Session.sendPreface();
}
http2Session.setupFilterChains(ctx, true);
httpResponse.setStatus(HttpStatus.OK_200);
httpResponse.getHeaders().clear();
httpRequest.setProtocol(Protocol.HTTP_2_0);
httpResponse.setProtocol(Protocol.HTTP_2_0);
httpResponse.getUpgradeDC().recycle();
httpResponse.getProcessingState().setKeepAlive(true);
if (http2Session.isGoingAway()) {
return false;
}
final Http2Stream stream = http2Session.openUpgradeStream(
httpRequest, 0);
final HttpContext oldHttpContext =
httpResponse.getProcessingState().getHttpContext();
final HttpContext httpContext = HttpContext.newInstance(stream,
stream, stream, httpRequest);
httpRequest.getProcessingState().setHttpContext(httpContext);
httpContext.attach(ctx);
final HttpRequestPacket dummyRequestPacket = HttpRequestPacket.builder()
.method(Method.PRI)
.uri("/dummy_pri")
.protocol(Protocol.HTTP_2_0)
.build();
final HttpResponsePacket dummyResponsePacket =
HttpResponsePacket.builder(dummyRequestPacket)
.status(200)
.reasonPhrase("OK")
.protocol(Protocol.HTTP_2_0)
.build();
dummyResponsePacket.getProcessingState().setHttpContext(oldHttpContext);
dummyResponsePacket.setIgnoreContentModifiers(true);
ctx.notifyDownstream(
HttpEvents.createChangePacketInProgressEvent(dummyResponsePacket));
return true;
}
private boolean (final Connection connection,
final Object msg) {
if (isNeverForceUpgrade) {
return false;
}
if (!HttpPacket.isHttp(msg)) {
return false;
}
final HttpHeader httpHeader = ((HttpPacket) msg).getHttpHeader();
if (!httpHeader.isRequest()
|| httpHeader.isUpgrade()
|| httpHeader.getProtocol() != Protocol.HTTP_1_1
|| httpHeader.containsHeader(Header.Connection)
) {
return false;
}
httpHeader.addHeader(Header.Upgrade, defaultHttp2Upgrade);
httpHeader.addHeader(Header.Connection, connectionUpgradeHeaderValue);
httpHeader.addHeader(Header.HTTP2Settings,
prepareSettings(Http2Session.get(connection)).build().toBase64Uri());
return true;
}
@Override
protected void (
final Http2Session http2Session,
final FilterChainContext context,
final HeaderBlockHead firstHeaderFrame) throws IOException {
if (!ignoreFrameForStreamId(http2Session, firstHeaderFrame.getStreamId())) {
switch (firstHeaderFrame.getType()) {
case PushPromiseFrame.TYPE:
processInPushPromise(http2Session, context,
(PushPromiseFrame) firstHeaderFrame);
break;
default:
processInResponse(http2Session, context,
(HeadersFrame) firstHeaderFrame);
}
}
}
@SuppressWarnings("DuplicateThrows")
private void (final Http2Session http2Session,
final FilterChainContext context,
final HeadersFrame headersFrame)
throws Http2SessionException, IOException {
final Http2Stream stream = http2Session.getStream(
headersFrame.getStreamId());
if (stream == null) {
return;
}
final HttpRequestPacket request = stream.getRequest();
HttpResponsePacket response = request.getResponse();
if (response == null) {
response = Http2Response.create();
}
final boolean isEOS = headersFrame.isEndStream();
bind(request, response);
stream.onRcvHeaders(isEOS);
final HttpContent content;
final Map<String,String> capture = ((NetLogger.isActive()) ? new LinkedHashMap<>() : null);
if (stream.getInboundHeaderFramesCounter() == 1) {
if (isEOS) {
response.setExpectContent(false);
stream.inputBuffer.terminate(IN_FIN_TERMINATION);
}
DecoderUtils.decodeResponseHeaders(http2Session, response, capture);
onHttpHeadersParsed(response, context);
response.getHeaders().mark();
content = response.httpContentBuilder().content(Buffers.EMPTY_BUFFER).last(isEOS).build();
} else {
DecoderUtils.decodeTrailerHeaders(http2Session, response, capture);
final HttpTrailer trailer = response.httpTrailerBuilder().content(Buffers.EMPTY_BUFFER).last(isEOS).build();
final MimeHeaders mimeHeaders = response.getHeaders();
if (mimeHeaders.trailerSize() > 0) {
for (final String name : mimeHeaders.trailerNames()) {
trailer.addHeader(name, mimeHeaders.getHeader(name));
}
}
content = trailer;
stream.flushInputData();
}
NetLogger.log(Context.RX, http2Session, headersFrame, capture);
if (isEOS) {
onHttpPacketParsed(response, context);
}
sendUpstream(http2Session, stream, content);
}
@SuppressWarnings("DuplicateThrows")
private void processInPushPromise(final Http2Session http2Session,
final FilterChainContext context,
final PushPromiseFrame pushPromiseFrame)
throws Http2StreamException, IOException {
if (http2Session.isGoingAway()) {
return;
}
final Http2Request request = Http2Request.create();
request.setConnection(context.getConnection());
final int refStreamId = pushPromiseFrame.getStreamId();
final Http2Stream refStream = http2Session.getStream(refStreamId);
if (refStream == null) {
throw new Http2StreamException(refStreamId, ErrorCode.REFUSED_STREAM,
"PushPromise is sent over unknown stream: " + refStreamId);
}
final Http2Stream stream = http2Session.acceptStream(request,
pushPromiseFrame.getPromisedStreamId(), refStreamId, false, 0);
final Map<String,String> capture = ((NetLogger.isActive()) ? new LinkedHashMap<>() : null);
DecoderUtils.decodeRequestHeaders(http2Session, request, capture);
NetLogger.log(Context.RX, http2Session, pushPromiseFrame, capture);
onHttpHeadersParsed(request, context);
prepareIncomingRequest(stream, request);
stream.outputSink.terminate(OUT_FIN_TERMINATION);
stream.onReceivePushPromise();
if (sendPushRequestUpstream) {
sendUpstream(http2Session,
stream,
request.httpContentBuilder().content(Buffers.EMPTY_BUFFER).last(false).build());
}
}
protected SettingsFrame.SettingsFrameBuilder prepareSettings(
final Http2Session http2Session) {
SettingsFrame.SettingsFrameBuilder builder = SettingsFrame.builder();
final int maxConcStreams = getConfiguration().getMaxConcurrentStreams();
if (maxConcStreams != -1 &&
maxConcStreams != http2Session.getDefaultMaxConcurrentStreams()) {
builder.setting(SETTINGS_MAX_CONCURRENT_STREAMS, maxConcStreams);
}
final int initWindSize = getConfiguration().getInitialWindowSize();
if (initWindSize != -1
&& http2Session != null
&& initWindSize != http2Session.getDefaultStreamWindowSize()) {
builder.setting(SETTINGS_INITIAL_WINDOW_SIZE, initWindSize);
}
builder.setting(SETTINGS_ENABLE_PUSH, ((getConfiguration().isPushEnabled()) ? 1 : 0));
return builder;
}
private void checkIfLastHttp11Chunk(final FilterChainContext ctx,
final Http2State http2State, final Object msg) {
if (HttpContent.isContent((HttpPacket) msg)) {
if (((HttpContent) msg).isLast()) {
http2State.onClientHttpUpgradeRequestFinished();
ctx.addCompletionListener(
new FilterChainContext.CompletionListener() {
@Override
public void onComplete(final FilterChainContext context) {
if (http2State.tryLockClientPreface()) {
final Http2Session http2Session =
http2State.getHttp2Session();
assert http2Session != null;
http2Session.sendPreface();
}
}
});
}
}
}
}