All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xnio.http.HttpUpgrade Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 33.0.2.Final
Show newest version
/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * 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.xnio.http;

import static org.xnio.IoUtils.safeClose;
import static org.xnio._private.Messages.msg;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.xnio.ChannelExceptionHandler;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.FutureResult;
import org.xnio.IoFuture;
import org.xnio.OptionMap;
import org.xnio.Pooled;
import org.xnio.StreamConnection;
import org.xnio.XnioWorker;
import org.xnio.channels.BoundChannel;
import org.xnio.ssl.SslConnection;
import org.xnio.channels.StreamSinkChannel;
import org.xnio.channels.StreamSourceChannel;
import org.xnio.conduits.PushBackStreamSourceConduit;
import org.xnio.conduits.StreamSourceConduit;
import org.xnio.ssl.XnioSsl;

/**
 * Simple HTTP client that can perform a HTTP upgrade. This is not a general purpose HTTP
 * client, all it can do is upgrade a HTTP
 *
 * @author Stuart Douglas
 */
public class HttpUpgrade {

    /**
     * Perform a HTTP upgrade that results in a SSL secured connection. This method should be used if the target endpoint is using https
     *
     * @param worker           The worker
     * @param ssl              The XnioSsl instance
     * @param bindAddress      The bind address
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param bindListener     The bind listener that is invoked when the socket is bound
     * @param optionMap        The option map for the connection
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static IoFuture performUpgrade(final XnioWorker worker, XnioSsl ssl, InetSocketAddress bindAddress, URI uri, final Map headers, ChannelListener openListener, ChannelListener bindListener, OptionMap optionMap, HandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(worker, ssl, bindAddress, uri, headers, openListener, bindListener, optionMap, handshakeChecker).doUpgrade();
    }


    /**
     * Perform a HTTP upgrade that results in a SSL secured connection. This method should be used if the target endpoint is using https
     *
     * @param worker           The worker
     * @param ssl              The XnioSsl instance
     * @param bindAddress      The bind address
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param bindListener     The bind listener that is invoked when the socket is bound
     * @param optionMap        The option map for the connection
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static IoFuture performUpgrade(final XnioWorker worker, XnioSsl ssl, InetSocketAddress bindAddress, URI uri, final Map> headers, ChannelListener openListener, ChannelListener bindListener, OptionMap optionMap, ExtendedHandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(worker, ssl, bindAddress, uri, headers, openListener, bindListener, optionMap, handshakeChecker).doUpgrade();
    }

    /**
     * Connects to the target server using HTTP upgrade.
     *
     * @param worker           The worker
     * @param bindAddress      The bind address
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param bindListener     The bind listener that is invoked when the socket is bound
     * @param optionMap        The option map for the connection
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static IoFuture performUpgrade(final XnioWorker worker, InetSocketAddress bindAddress, URI uri, final Map headers, ChannelListener openListener, ChannelListener bindListener, OptionMap optionMap, HandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(worker, null, bindAddress, uri, headers, openListener, bindListener, optionMap, handshakeChecker).doUpgrade();
    }

    /**
     * Connects to the target server using HTTP upgrade.
     *
     * @param worker           The worker
     * @param bindAddress      The bind address
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param bindListener     The bind listener that is invoked when the socket is bound
     * @param optionMap        The option map for the connection
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static IoFuture performUpgrade(final XnioWorker worker, InetSocketAddress bindAddress, URI uri, final Map> headers, ChannelListener openListener, ChannelListener bindListener, OptionMap optionMap, ExtendedHandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(worker, null, bindAddress, uri, headers, openListener, bindListener, optionMap, handshakeChecker).doUpgrade();
    }


    /**
     * Performs a HTTP upgrade on an existing connection.
     *
     * @param connection       The existing connection to upgrade
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static  IoFuture performUpgrade(final T connection, URI uri, final Map headers, ChannelListener openListener, HandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(connection, uri, headers, openListener, handshakeChecker).upgradeExistingConnection();
    }

    /**
     * Performs a HTTP upgrade on an existing connection.
     *
     * @param connection       The existing connection to upgrade
     * @param uri              The URI to connect to
     * @param headers          Any additional headers to include in the upgrade request. This must include an Upgrade header that specifies the type of upgrade being performed
     * @param openListener     The open listener that is invoked once the HTTP upgrade is done
     * @param handshakeChecker A handshake checker that can be supplied to verify that the server returned a valid response to the upgrade request
     * @return An IoFuture of the connection
     */
    public static  IoFuture performUpgrade(final T connection, URI uri, final Map> headers, ChannelListener openListener, ExtendedHandshakeChecker handshakeChecker) {
        return new HttpUpgradeState(connection, uri, headers, openListener, handshakeChecker).upgradeExistingConnection();
    }

    private HttpUpgrade() {

    }

    private static class HttpUpgradeState {


        private final XnioWorker worker;
        private final XnioSsl ssl;
        private final InetSocketAddress bindAddress;
        private final URI uri;
        private final Map> headers;
        private final ChannelListener openListener;
        private final ChannelListener bindListener;
        private final OptionMap optionMap;
        private final Object handshakeChecker;
        private final FutureResult future = new FutureResult();
        private T connection;


        private HttpUpgradeState(final XnioWorker worker, final XnioSsl ssl, final InetSocketAddress bindAddress, final URI uri, final Map headers, final ChannelListener openListener, final ChannelListener bindListener, final OptionMap optionMap, final HandshakeChecker handshakeChecker) {
            this.worker = worker;
            this.ssl = ssl;
            this.bindAddress = bindAddress;
            this.uri = uri;
            this.openListener = openListener;
            this.bindListener = bindListener;
            this.optionMap = optionMap;
            this.handshakeChecker = handshakeChecker;
            Map> newHeaders = new HashMap<>();
            for(Map.Entry entry  : headers.entrySet()) {
                newHeaders.put(entry.getKey(), Collections.singletonList(entry.getValue()));
            }
            this.headers = newHeaders;
        }

        private HttpUpgradeState(final XnioWorker worker, final XnioSsl ssl, final InetSocketAddress bindAddress, final URI uri, final Map> headers, final ChannelListener openListener, final ChannelListener bindListener, final OptionMap optionMap, final ExtendedHandshakeChecker handshakeChecker) {
            this.worker = worker;
            this.ssl = ssl;
            this.bindAddress = bindAddress;
            this.uri = uri;
            this.headers = headers;
            this.openListener = openListener;
            this.bindListener = bindListener;
            this.optionMap = optionMap;
            this.handshakeChecker = handshakeChecker;
        }

        public HttpUpgradeState(final T connection, final URI uri, final Map headers, final ChannelListener openListener, final HandshakeChecker handshakeChecker) {
            this.worker = connection.getWorker();
            this.ssl = null;
            this.bindAddress = null;
            this.uri = uri;
            this.openListener = openListener;
            this.bindListener = null;
            this.optionMap = OptionMap.EMPTY;
            this.handshakeChecker = handshakeChecker;
            this.connection = connection;
            Map> newHeaders = new HashMap<>();
            for(Map.Entry entry  : headers.entrySet()) {
                newHeaders.put(entry.getKey(), Collections.singletonList(entry.getValue()));
            }
            this.headers = newHeaders;
        }

        public HttpUpgradeState(final T connection, final URI uri, final Map> headers, final ChannelListener openListener, final ExtendedHandshakeChecker handshakeChecker) {
            this.worker = connection.getWorker();
            this.ssl = null;
            this.bindAddress = null;
            this.uri = uri;
            this.headers = headers;
            this.openListener = openListener;
            this.bindListener = null;
            this.optionMap = OptionMap.EMPTY;
            this.handshakeChecker = handshakeChecker;
            this.connection = connection;
        }


        private IoFuture doUpgrade() {
            InetSocketAddress address = new InetSocketAddress(uri.getHost(), uri.getPort());

            final ChannelListener connectListener = new ConnectionOpenListener();
            final String scheme = uri.getScheme();
            if (scheme.equals("http")) {
                if (bindAddress == null) {
                    worker.openStreamConnection(address, connectListener, bindListener, optionMap).addNotifier(new FailureNotifier(), null);
                } else {
                    worker.openStreamConnection(bindAddress, address, connectListener, bindListener, optionMap).addNotifier(new FailureNotifier(), null);
                }
            } else if (scheme.equals("https")) {
                if (ssl == null) {
                    throw msg.missingSslProvider();
                }
                if (bindAddress == null) {
                    ssl.openSslConnection(worker, address, connectListener, bindListener, optionMap).addNotifier(new FailureNotifier(), null);
                } else {
                    ssl.openSslConnection(worker, bindAddress, address, connectListener, bindListener, optionMap).addNotifier(new FailureNotifier(), null);
                }
            } else {
                throw msg.invalidURLScheme(scheme);
            }
            return future.getIoFuture();
        }

        private String buildHttpRequest() {

            final StringBuilder builder = new StringBuilder();
            builder.append("GET ");
            builder.append(uri.getPath().isEmpty() ? "/" : uri.getPath());
            if(uri.getQuery() != null && !uri.getQuery().isEmpty()) {
                builder.append('?');
                builder.append(uri.getQuery());
            }
            builder.append(" HTTP/1.1\r\n");
            final Set seen = new HashSet();
            for (Map.Entry> headerEntry : headers.entrySet()) {
                for(String value : headerEntry.getValue()) {
                    builder.append(headerEntry.getKey());
                    builder.append(": ");
                    builder.append(value);
                    builder.append("\r\n");
                    seen.add(headerEntry.getKey().toLowerCase(Locale.ENGLISH));
                }
            }
            if (!seen.contains("host")) {
                builder.append("Host: ");
                builder.append(getHost());
                builder.append("\r\n");
            }
            if (!seen.contains("connection")) {
                builder.append("Connection: upgrade\r\n");
            }
            if (!seen.contains("upgrade")) {
                throw new IllegalArgumentException("Upgrade: header was not supplied in header arguments");
            }
            builder.append("\r\n");
            return builder.toString();
        }

        private String getHost() {
            String scheme = uri.getScheme();
            int port = uri.getPort();

            if (port < 0 || "http".equals(scheme) && port == 80 || "https".equals(scheme) && port == 443) {
                // No port or default port.
                return uri.getHost();
            }

            return uri.getHost() + ":" + port;
        }

        public IoFuture upgradeExistingConnection() {
            final ChannelListener connectListener = new ConnectionOpenListener();
            connectListener.handleEvent(connection);
            return future.getIoFuture();
        }


        private class ConnectionOpenListener implements ChannelListener {
            @Override
            public void handleEvent(final StreamConnection channel) {
                connection = (T) channel;
                final ByteBuffer buffer = ByteBuffer.wrap(buildHttpRequest().getBytes());
                int r;
                do {
                    try {
                        r = channel.getSinkChannel().write(buffer);
                        if (r == 0) {
                            channel.getSinkChannel().getWriteSetter().set(new StringWriteListener(buffer));
                            channel.getSinkChannel().resumeWrites();
                            return;
                        }
                    } catch (IOException e) {
                        safeClose(channel);
                        future.setException(e);
                        return;
                    }
                } while (buffer.hasRemaining());
                flushUpgradeChannel();
            }
        }

        private void flushUpgradeChannel() {
            try {
                if(!connection.getSinkChannel().flush()) {

                    connection.getSinkChannel().getWriteSetter().set(ChannelListeners.flushingChannelListener(new ChannelListener() {
                        @Override
                        public void handleEvent(StreamSinkChannel channel) {
                            channel.suspendWrites();
                            new UpgradeResultListener().handleEvent(connection.getSourceChannel());
                        }
                    }, new ChannelExceptionHandler() {
                        @Override
                        public void handleException(StreamSinkChannel channel, IOException exception) {
                            safeClose(channel);
                            future.setException(exception);
                        }
                    }));
                    connection.getSinkChannel().resumeWrites();
                    return;
                }
            } catch (IOException e) {
                safeClose(connection);
                future.setException(e);
                return;
            }
            new UpgradeResultListener().handleEvent(connection.getSourceChannel());
        }

        private final class StringWriteListener implements ChannelListener {

            final ByteBuffer buffer;

            private StringWriteListener(final ByteBuffer buffer) {
                this.buffer = buffer;
            }

            @Override
            public void handleEvent(final StreamSinkChannel channel) {
                int r;
                do {
                    try {
                        r = channel.write(buffer);
                        if (r == 0) {
                            return;
                        }
                    } catch (IOException e) {
                        safeClose(channel);
                        future.setException(e);
                        return;
                    }
                } while (buffer.hasRemaining());
                channel.suspendWrites();
                flushUpgradeChannel();
            }
        }

        private final class UpgradeResultListener implements ChannelListener {

            private final HttpUpgradeParser parser = new HttpUpgradeParser();
            private ByteBuffer buffer = ByteBuffer.allocate(1024);

            @Override
            public void handleEvent(final StreamSourceChannel channel) {
                int r;
                do {
                    try {
                        r = channel.read(buffer);
                        if (r == 0) {
                            channel.getReadSetter().set(this);
                            channel.resumeReads();
                            return;
                        } else if (r == -1) {
                            throw msg.connectionClosedEarly();
                        }
                        buffer.flip();
                        parser.parse(buffer);
                        if(!parser.isComplete()) {
                            buffer.compact();
                        }
                    } catch (IOException e) {
                        safeClose(channel);
                        future.setException(e);
                        return;
                    }

                } while (!parser.isComplete());
                channel.suspendReads();

                if (buffer.hasRemaining()) {
                    StreamSourceConduit orig = connection.getSourceChannel().getConduit();
                    PushBackStreamSourceConduit pushBack = new PushBackStreamSourceConduit(orig);
                    pushBack.pushBack(new Pooled() {
                        @Override
                        public void discard() {
                            buffer = null;
                        }

                        @Override
                        public void free() {
                            buffer = null;
                        }

                        @Override
                        public ByteBuffer getResource() throws IllegalStateException {
                            return buffer;
                        }

                        @Override
                        public void close() {
                            free();
                        }
                    });
                    connection.getSourceChannel().setConduit(pushBack);
                }

                //ok, we have a response
                if (parser.getResponseCode() == 101) { // Switching Protocols
                    handleUpgrade(parser);
                } else if (parser.getResponseCode() == 301 || // Moved Permanently
                        parser.getResponseCode() == 302 || // Found
                        parser.getResponseCode() == 303 || // See Other
                        parser.getResponseCode() == 307 || // Temporary Redirect
                        parser.getResponseCode() == 308) { // Permanent Redirect
                    safeClose(connection);
                    handleRedirect(parser);
                } else {
                    safeClose(connection);
                    future.setException(new UpgradeFailedException("Invalid response code " + parser.getResponseCode()));
                }
            }
        }

        private void handleUpgrade(final HttpUpgradeParser parser) {
            Map simpleHeaders = new HashMap<>();
            for(Map.Entry> e : parser.getHeaders().entrySet()) {
                simpleHeaders.put(e.getKey(), e.getValue().get(0));
            }
            final String contentLength = simpleHeaders.get("content-length");
            if (contentLength != null) {
                if (!"0".equals(contentLength)) {
                    future.setException(new IOException("Upgrade responses must have a content length of zero."));
                    return;
                }
            }
            final String transferCoding = simpleHeaders.get("transfer-encoding");
            if (transferCoding != null) {
                future.setException(new IOException("Upgrade responses cannot have a transfer coding"));
                return;
            }

            if (handshakeChecker != null) {
                try {
                    if(handshakeChecker instanceof ExtendedHandshakeChecker) {
                        ((ExtendedHandshakeChecker) handshakeChecker).checkHandshakeExtended(parser.getHeaders());
                    } else {
                        ((HandshakeChecker)handshakeChecker).checkHandshake(simpleHeaders);
                    }
                } catch (IOException e) {
                    safeClose(connection);
                    future.setException(e);
                    return;
                }
            }
            future.setResult(connection);
            ChannelListeners.invokeChannelListener(connection, openListener);
        }

        private void handleRedirect(final HttpUpgradeParser parser) {
            List location = parser.getHeaders().get("location");
            future.setException(new RedirectException(msg.redirect(), parser.getResponseCode(), location == null ? null : location.get(0)));
        }

        private class FailureNotifier extends IoFuture.HandlingNotifier {
            @Override
            public void handleFailed(IOException exception, Object attachment) {
                future.setException(exception);
            }

            @Override
            public void handleCancelled(Object attachment) {
                future.setCancelled();
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy