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}