001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.devtools.tunnel.client;
018
019import java.io.Closeable;
020import java.io.IOException;
021import java.net.ConnectException;
022import java.net.MalformedURLException;
023import java.net.URI;
024import java.net.URISyntaxException;
025import java.net.URL;
026import java.nio.ByteBuffer;
027import java.nio.channels.WritableByteChannel;
028import java.util.concurrent.Executor;
029import java.util.concurrent.Executors;
030import java.util.concurrent.ThreadFactory;
031import java.util.concurrent.atomic.AtomicLong;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035
036import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
037import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder;
038import org.springframework.http.HttpMethod;
039import org.springframework.http.HttpStatus;
040import org.springframework.http.client.ClientHttpRequest;
041import org.springframework.http.client.ClientHttpRequestFactory;
042import org.springframework.http.client.ClientHttpResponse;
043import org.springframework.util.Assert;
044
045/**
046 * {@link TunnelConnection} implementation that uses HTTP to transfer data.
047 *
048 * @author Phillip Webb
049 * @author Rob Winch
050 * @author Andy Wilkinson
051 * @since 1.3.0
052 * @see TunnelClient
053 * @see org.springframework.boot.devtools.tunnel.server.HttpTunnelServer
054 */
055public class HttpTunnelConnection implements TunnelConnection {
056
057        private static final Log logger = LogFactory.getLog(HttpTunnelConnection.class);
058
059        private final URI uri;
060
061        private final ClientHttpRequestFactory requestFactory;
062
063        private final Executor executor;
064
065        /**
066         * Create a new {@link HttpTunnelConnection} instance.
067         * @param url the URL to connect to
068         * @param requestFactory the HTTP request factory
069         */
070        public HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory) {
071                this(url, requestFactory, null);
072        }
073
074        /**
075         * Create a new {@link HttpTunnelConnection} instance.
076         * @param url the URL to connect to
077         * @param requestFactory the HTTP request factory
078         * @param executor the executor used to handle connections
079         */
080        protected HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory,
081                        Executor executor) {
082                Assert.hasLength(url, "URL must not be empty");
083                Assert.notNull(requestFactory, "RequestFactory must not be null");
084                try {
085                        this.uri = new URL(url).toURI();
086                }
087                catch (URISyntaxException | MalformedURLException ex) {
088                        throw new IllegalArgumentException("Malformed URL '" + url + "'");
089                }
090                this.requestFactory = requestFactory;
091                this.executor = (executor != null) ? executor
092                                : Executors.newCachedThreadPool(new TunnelThreadFactory());
093        }
094
095        @Override
096        public TunnelChannel open(WritableByteChannel incomingChannel, Closeable closeable)
097                        throws Exception {
098                logger.trace("Opening HTTP tunnel to " + this.uri);
099                return new TunnelChannel(incomingChannel, closeable);
100        }
101
102        protected final ClientHttpRequest createRequest(boolean hasPayload)
103                        throws IOException {
104                HttpMethod method = hasPayload ? HttpMethod.POST : HttpMethod.GET;
105                return this.requestFactory.createRequest(this.uri, method);
106        }
107
108        /**
109         * A {@link WritableByteChannel} used to transfer traffic.
110         */
111        protected class TunnelChannel implements WritableByteChannel {
112
113                private final HttpTunnelPayloadForwarder forwarder;
114
115                private final Closeable closeable;
116
117                private boolean open = true;
118
119                private AtomicLong requestSeq = new AtomicLong();
120
121                public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) {
122                        this.forwarder = new HttpTunnelPayloadForwarder(incomingChannel);
123                        this.closeable = closeable;
124                        openNewConnection(null);
125                }
126
127                @Override
128                public boolean isOpen() {
129                        return this.open;
130                }
131
132                @Override
133                public void close() throws IOException {
134                        if (this.open) {
135                                this.open = false;
136                                this.closeable.close();
137                        }
138                }
139
140                @Override
141                public int write(ByteBuffer src) throws IOException {
142                        int size = src.remaining();
143                        if (size > 0) {
144                                openNewConnection(
145                                                new HttpTunnelPayload(this.requestSeq.incrementAndGet(), src));
146                        }
147                        return size;
148                }
149
150                private void openNewConnection(HttpTunnelPayload payload) {
151                        HttpTunnelConnection.this.executor.execute(new Runnable() {
152
153                                @Override
154                                public void run() {
155                                        try {
156                                                sendAndReceive(payload);
157                                        }
158                                        catch (IOException ex) {
159                                                if (ex instanceof ConnectException) {
160                                                        logger.warn("Failed to connect to remote application at "
161                                                                        + HttpTunnelConnection.this.uri);
162                                                }
163                                                else {
164                                                        logger.trace("Unexpected connection error", ex);
165                                                }
166                                                closeQuietly();
167                                        }
168                                }
169
170                                private void closeQuietly() {
171                                        try {
172                                                close();
173                                        }
174                                        catch (IOException ex) {
175                                                // Ignore
176                                        }
177                                }
178
179                        });
180                }
181
182                private void sendAndReceive(HttpTunnelPayload payload) throws IOException {
183                        ClientHttpRequest request = createRequest(payload != null);
184                        if (payload != null) {
185                                payload.logIncoming();
186                                payload.assignTo(request);
187                        }
188                        handleResponse(request.execute());
189                }
190
191                private void handleResponse(ClientHttpResponse response) throws IOException {
192                        if (response.getStatusCode() == HttpStatus.GONE) {
193                                close();
194                                return;
195                        }
196                        if (response.getStatusCode() == HttpStatus.OK) {
197                                HttpTunnelPayload payload = HttpTunnelPayload.get(response);
198                                if (payload != null) {
199                                        this.forwarder.forward(payload);
200                                }
201                        }
202                        if (response.getStatusCode() != HttpStatus.TOO_MANY_REQUESTS) {
203                                openNewConnection(null);
204                        }
205                }
206
207        }
208
209        /**
210         * {@link ThreadFactory} used to create the tunnel thread.
211         */
212        private static class TunnelThreadFactory implements ThreadFactory {
213
214                @Override
215                public Thread newThread(Runnable runnable) {
216                        Thread thread = new Thread(runnable, "HTTP Tunnel Connection");
217                        thread.setDaemon(true);
218                        return thread;
219                }
220
221        }
222
223}