001/*
002 * Copyright 2012-2016 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 ex) {
088                        throw new IllegalArgumentException("Malformed URL '" + url + "'");
089                }
090                catch (MalformedURLException ex) {
091                        throw new IllegalArgumentException("Malformed URL '" + url + "'");
092                }
093                this.requestFactory = requestFactory;
094                this.executor = (executor == null
095                                ? Executors.newCachedThreadPool(new TunnelThreadFactory()) : executor);
096        }
097
098        @Override
099        public TunnelChannel open(WritableByteChannel incomingChannel, Closeable closeable)
100                        throws Exception {
101                logger.trace("Opening HTTP tunnel to " + this.uri);
102                return new TunnelChannel(incomingChannel, closeable);
103        }
104
105        protected final ClientHttpRequest createRequest(boolean hasPayload)
106                        throws IOException {
107                HttpMethod method = (hasPayload ? HttpMethod.POST : HttpMethod.GET);
108                return this.requestFactory.createRequest(this.uri, method);
109        }
110
111        /**
112         * A {@link WritableByteChannel} used to transfer traffic.
113         */
114        protected class TunnelChannel implements WritableByteChannel {
115
116                private final HttpTunnelPayloadForwarder forwarder;
117
118                private final Closeable closeable;
119
120                private boolean open = true;
121
122                private AtomicLong requestSeq = new AtomicLong();
123
124                public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) {
125                        this.forwarder = new HttpTunnelPayloadForwarder(incomingChannel);
126                        this.closeable = closeable;
127                        openNewConnection(null);
128                }
129
130                @Override
131                public boolean isOpen() {
132                        return this.open;
133                }
134
135                @Override
136                public void close() throws IOException {
137                        if (this.open) {
138                                this.open = false;
139                                this.closeable.close();
140                        }
141                }
142
143                @Override
144                public int write(ByteBuffer src) throws IOException {
145                        int size = src.remaining();
146                        if (size > 0) {
147                                openNewConnection(
148                                                new HttpTunnelPayload(this.requestSeq.incrementAndGet(), src));
149                        }
150                        return size;
151                }
152
153                private void openNewConnection(final HttpTunnelPayload payload) {
154                        HttpTunnelConnection.this.executor.execute(new Runnable() {
155
156                                @Override
157                                public void run() {
158                                        try {
159                                                sendAndReceive(payload);
160                                        }
161                                        catch (IOException ex) {
162                                                if (ex instanceof ConnectException) {
163                                                        logger.warn("Failed to connect to remote application at "
164                                                                        + HttpTunnelConnection.this.uri);
165                                                }
166                                                else {
167                                                        logger.trace("Unexpected connection error", ex);
168                                                }
169                                                closeQuietly();
170                                        }
171                                }
172
173                                private void closeQuietly() {
174                                        try {
175                                                close();
176                                        }
177                                        catch (IOException ex) {
178                                                // Ignore
179                                        }
180                                }
181
182                        });
183                }
184
185                private void sendAndReceive(HttpTunnelPayload payload) throws IOException {
186                        ClientHttpRequest request = createRequest(payload != null);
187                        if (payload != null) {
188                                payload.logIncoming();
189                                payload.assignTo(request);
190                        }
191                        handleResponse(request.execute());
192                }
193
194                private void handleResponse(ClientHttpResponse response) throws IOException {
195                        if (response.getStatusCode() == HttpStatus.GONE) {
196                                close();
197                                return;
198                        }
199                        if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE) {
200                                logger.warn("Remote application responded with service unavailable. Did "
201                                                + "you forget to start it with remote debugging enabled?");
202                                close();
203                                return;
204                        }
205                        if (response.getStatusCode() == HttpStatus.OK) {
206                                HttpTunnelPayload payload = HttpTunnelPayload.get(response);
207                                if (payload != null) {
208                                        this.forwarder.forward(payload);
209                                }
210                        }
211                        if (response.getStatusCode() != HttpStatus.TOO_MANY_REQUESTS) {
212                                openNewConnection(null);
213                        }
214                }
215
216        }
217
218        /**
219         * {@link ThreadFactory} used to create the tunnel thread.
220         */
221        private static class TunnelThreadFactory implements ThreadFactory {
222
223                @Override
224                public Thread newThread(Runnable runnable) {
225                        Thread thread = new Thread(runnable, "HTTP Tunnel Connection");
226                        thread.setDaemon(true);
227                        return thread;
228                }
229
230        }
231
232}