/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 io.undertow.server.protocol.http2;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.xnio.OptionMap;
import org.xnio.StreamConnection;

import io.undertow.UndertowLogger;
import io.undertow.UndertowOptions;
import io.undertow.io.IoCallback;
import io.undertow.io.Receiver;
import io.undertow.io.Sender;
import io.undertow.protocols.http2.Http2Channel;
import io.undertow.server.Connectors;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.HttpUpgradeListener;
import io.undertow.server.protocol.http.HttpContinue;
import io.undertow.util.FlexBase64;
import io.undertow.util.Headers;
import io.undertow.util.ImmediatePooledByteBuffer;
import io.undertow.util.Protocols;
import io.undertow.util.StatusCodes;

Upgrade listener for HTTP2, this allows connections to be established using the upgrade mechanism as detailed in Section 3.2. This should always be the first handler in a handler chain.
Author:Stuart Douglas
/** * Upgrade listener for HTTP2, this allows connections to be established using the upgrade * mechanism as detailed in Section 3.2. This should always be the first handler in a handler * chain. * * * @author Stuart Douglas */
public class Http2UpgradeHandler implements HttpHandler { private final HttpHandler next; private final Set<String> upgradeStrings; public Http2UpgradeHandler(HttpHandler next) { this.next = next; this.upgradeStrings = Collections.singleton(Http2Channel.CLEARTEXT_UPGRADE_STRING); } public Http2UpgradeHandler(HttpHandler next, String... upgradeStrings) { this.next = next; this.upgradeStrings = new HashSet<>(Arrays.asList(upgradeStrings)); } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { final String upgrade = exchange.getRequestHeaders().getFirst(Headers.UPGRADE); final String settings = exchange.getRequestHeaders().getFirst("HTTP2-Settings"); if(settings != null && upgrade != null && upgradeStrings.contains(upgrade)) { if(HttpContinue.requiresContinueResponse(exchange) && false) { HttpContinue.sendContinueResponse(exchange, new IoCallback() { @Override public void onComplete(HttpServerExchange exchange, Sender sender) { try { handleUpgradeBody(exchange, upgrade, settings); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void onException(HttpServerExchange exchange, Sender sender, IOException exception) { exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); exchange.endExchange(); } }); } else { handleUpgradeBody(exchange, upgrade, settings); } return; } next.handleRequest(exchange); } private void handleUpgradeBody(HttpServerExchange exchange, String upgrade, String settings) throws Exception { if(exchange.isRequestComplete()) { handleHttp2Upgrade(exchange, upgrade, settings, null); } else { final int maxBufferedSize = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_BUFFERED_REQUEST_SIZE, UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE); if(exchange.getRequestContentLength() > maxBufferedSize) { //request is too big to buffer //we don't upgrade to HTTP/2 next.handleRequest(exchange); } else if(exchange.getRequestContentLength() > 0 && exchange.getRequestContentLength() < maxBufferedSize) { //we know it is fine to buffer exchange.getRequestReceiver().receiveFullBytes(new Receiver.FullBytesCallback() { @Override public void handle(HttpServerExchange exchange, byte[] message) { try { handleHttp2Upgrade(exchange, upgrade, settings, message); } catch (IOException e) { UndertowLogger.REQUEST_IO_LOGGER.ioException(e); exchange.endExchange(); } } }); } else { final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); exchange.getRequestReceiver().receivePartialBytes(new Receiver.PartialBytesCallback() { @Override public void handle(HttpServerExchange exchange, byte[] message, boolean last) { try { outputStream.write(message); if(last) { handleHttp2Upgrade(exchange, upgrade, settings, outputStream.toByteArray()); } else if(outputStream.size() >= maxBufferedSize) { exchange.getRequestReceiver().pause(); Connectors.ungetRequestBytes(exchange, new ImmediatePooledByteBuffer(ByteBuffer.wrap(outputStream.toByteArray()))); Connectors.resetRequestChannel(exchange); next.handleRequest(exchange); } } catch (IOException e) { UndertowLogger.REQUEST_IO_LOGGER.ioException(e); exchange.endExchange(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } }); } } } private void handleHttp2Upgrade(HttpServerExchange exchange, final String upgrade, String settings, final byte[] data) throws IOException { //required by spec final ByteBuffer settingsFrame = FlexBase64.decodeURL(settings); exchange.getResponseHeaders().put(Headers.UPGRADE, upgrade); exchange.upgradeChannel(new HttpUpgradeListener() { @Override public void handleUpgrade(StreamConnection streamConnection, HttpServerExchange exchange) { OptionMap undertowOptions = exchange.getConnection().getUndertowOptions(); Http2Channel channel = new Http2Channel(streamConnection, upgrade, exchange.getConnection().getByteBufferPool(), null, false, true, true, settingsFrame, undertowOptions); Http2ReceiveListener receiveListener = new Http2ReceiveListener(new HttpHandler() { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { //if this header is present we don't actually process the rest of the handler chain //as the request was only to create the initial request if(exchange.getRequestHeaders().contains("X-HTTP2-connect-only")) { exchange.endExchange(); return; } exchange.setProtocol(Protocols.HTTP_2_0); next.handleRequest(exchange); } }, undertowOptions, exchange.getConnection().getBufferSize(), null); channel.getReceiveSetter().set(receiveListener); receiveListener.handleInitialRequest(exchange, channel, data); channel.resumeReceives(); } }); } }