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}