diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnection.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnection.java deleted file mode 100644 index cf3a9c3335..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnection.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.io.Closeable; -import java.io.IOException; -import java.net.ConnectException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.channels.WritableByteChannel; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicLong; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload; -import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder; -import org.springframework.core.log.LogMessage; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.util.Assert; - -/** - * {@link TunnelConnection} implementation that uses HTTP to transfer data. - * - * @author Phillip Webb - * @author Rob Winch - * @author Andy Wilkinson - * @since 1.3.0 - * @see TunnelClient - * @see org.springframework.boot.devtools.tunnel.server.HttpTunnelServer - */ -public class HttpTunnelConnection implements TunnelConnection { - - private static final Log logger = LogFactory.getLog(HttpTunnelConnection.class); - - private final URI uri; - - private final ClientHttpRequestFactory requestFactory; - - private final Executor executor; - - /** - * Create a new {@link HttpTunnelConnection} instance. - * @param url the URL to connect to - * @param requestFactory the HTTP request factory - */ - public HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory) { - this(url, requestFactory, null); - } - - /** - * Create a new {@link HttpTunnelConnection} instance. - * @param url the URL to connect to - * @param requestFactory the HTTP request factory - * @param executor the executor used to handle connections - */ - protected HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory, Executor executor) { - Assert.hasLength(url, "URL must not be empty"); - Assert.notNull(requestFactory, "RequestFactory must not be null"); - try { - this.uri = new URL(url).toURI(); - } - catch (URISyntaxException | MalformedURLException ex) { - throw new IllegalArgumentException("Malformed URL '" + url + "'"); - } - this.requestFactory = requestFactory; - this.executor = (executor != null) ? executor : Executors.newCachedThreadPool(new TunnelThreadFactory()); - } - - @Override - public TunnelChannel open(WritableByteChannel incomingChannel, Closeable closeable) throws Exception { - logger.trace(LogMessage.format("Opening HTTP tunnel to %s", this.uri)); - return new TunnelChannel(incomingChannel, closeable); - } - - protected final ClientHttpRequest createRequest(boolean hasPayload) throws IOException { - HttpMethod method = hasPayload ? HttpMethod.POST : HttpMethod.GET; - return this.requestFactory.createRequest(this.uri, method); - } - - /** - * A {@link WritableByteChannel} used to transfer traffic. - */ - protected class TunnelChannel implements WritableByteChannel { - - private final HttpTunnelPayloadForwarder forwarder; - - private final Closeable closeable; - - private boolean open = true; - - private AtomicLong requestSeq = new AtomicLong(); - - public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) { - this.forwarder = new HttpTunnelPayloadForwarder(incomingChannel); - this.closeable = closeable; - openNewConnection(null); - } - - @Override - public boolean isOpen() { - return this.open; - } - - @Override - public void close() throws IOException { - if (this.open) { - this.open = false; - this.closeable.close(); - } - } - - @Override - public int write(ByteBuffer src) throws IOException { - int size = src.remaining(); - if (size > 0) { - openNewConnection(new HttpTunnelPayload(this.requestSeq.incrementAndGet(), src)); - } - return size; - } - - private void openNewConnection(HttpTunnelPayload payload) { - HttpTunnelConnection.this.executor.execute(new Runnable() { - - @Override - public void run() { - try { - sendAndReceive(payload); - } - catch (IOException ex) { - if (ex instanceof ConnectException) { - logger.warn(LogMessage.format("Failed to connect to remote application at %s", - HttpTunnelConnection.this.uri)); - } - else { - logger.trace("Unexpected connection error", ex); - } - closeQuietly(); - } - } - - private void closeQuietly() { - try { - close(); - } - catch (IOException ex) { - // Ignore - } - } - - }); - } - - private void sendAndReceive(HttpTunnelPayload payload) throws IOException { - ClientHttpRequest request = createRequest(payload != null); - if (payload != null) { - payload.logIncoming(); - payload.assignTo(request); - } - handleResponse(request.execute()); - } - - private void handleResponse(ClientHttpResponse response) throws IOException { - if (response.getStatusCode() == HttpStatus.GONE) { - close(); - return; - } - if (response.getStatusCode() == HttpStatus.OK) { - HttpTunnelPayload payload = HttpTunnelPayload.get(response); - if (payload != null) { - this.forwarder.forward(payload); - } - } - if (response.getStatusCode() != HttpStatus.TOO_MANY_REQUESTS) { - openNewConnection(null); - } - } - - } - - /** - * {@link ThreadFactory} used to create the tunnel thread. - */ - private static class TunnelThreadFactory implements ThreadFactory { - - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable, "HTTP Tunnel Connection"); - thread.setDaemon(true); - return thread; - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java deleted file mode 100644 index e4a439492d..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.nio.ByteBuffer; -import java.nio.channels.AsynchronousCloseException; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.nio.channels.WritableByteChannel; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.core.log.LogMessage; -import org.springframework.util.Assert; - -/** - * The client side component of a socket tunnel. Starts a {@link ServerSocket} of the - * specified port for local clients to connect to. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.3.0 - */ -public class TunnelClient implements SmartInitializingSingleton { - - private static final int BUFFER_SIZE = 1024 * 100; - - private static final Log logger = LogFactory.getLog(TunnelClient.class); - - private final TunnelClientListeners listeners = new TunnelClientListeners(); - - private final Object monitor = new Object(); - - private final int listenPort; - - private final TunnelConnection tunnelConnection; - - private ServerThread serverThread; - - public TunnelClient(int listenPort, TunnelConnection tunnelConnection) { - Assert.isTrue(listenPort >= 0, "ListenPort must be greater than or equal to 0"); - Assert.notNull(tunnelConnection, "TunnelConnection must not be null"); - this.listenPort = listenPort; - this.tunnelConnection = tunnelConnection; - } - - @Override - public void afterSingletonsInstantiated() { - synchronized (this.monitor) { - if (this.serverThread == null) { - try { - start(); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - } - } - - /** - * Start the client and accept incoming connections. - * @return the port on which the client is listening - * @throws IOException in case of I/O errors - */ - public int start() throws IOException { - synchronized (this.monitor) { - Assert.state(this.serverThread == null, "Server already started"); - ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort)); - int port = serverSocketChannel.socket().getLocalPort(); - logger.trace(LogMessage.format("Listening for TCP traffic to tunnel on port %s", port)); - this.serverThread = new ServerThread(serverSocketChannel); - this.serverThread.start(); - return port; - } - } - - /** - * Stop the client, disconnecting any servers. - * @throws IOException in case of I/O errors - */ - public void stop() throws IOException { - synchronized (this.monitor) { - if (this.serverThread != null) { - this.serverThread.close(); - try { - this.serverThread.join(2000); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.serverThread = null; - } - } - } - - protected final ServerThread getServerThread() { - synchronized (this.monitor) { - return this.serverThread; - } - } - - public void addListener(TunnelClientListener listener) { - this.listeners.addListener(listener); - } - - public void removeListener(TunnelClientListener listener) { - this.listeners.removeListener(listener); - } - - /** - * The main server thread. - */ - protected class ServerThread extends Thread { - - private final ServerSocketChannel serverSocketChannel; - - private boolean acceptConnections = true; - - public ServerThread(ServerSocketChannel serverSocketChannel) { - this.serverSocketChannel = serverSocketChannel; - setName("Tunnel Server"); - setDaemon(true); - } - - public void close() throws IOException { - logger.trace(LogMessage.format("Closing tunnel client on port %s", - this.serverSocketChannel.socket().getLocalPort())); - this.serverSocketChannel.close(); - this.acceptConnections = false; - interrupt(); - } - - @Override - public void run() { - try { - while (this.acceptConnections) { - try (SocketChannel socket = this.serverSocketChannel.accept()) { - handleConnection(socket); - } - catch (AsynchronousCloseException ex) { - // Connection has been closed. Keep the server running - } - } - } - catch (Exception ex) { - logger.trace("Unexpected exception from tunnel client", ex); - } - } - - private void handleConnection(SocketChannel socketChannel) throws Exception { - Closeable closeable = new SocketCloseable(socketChannel); - TunnelClient.this.listeners.fireOpenEvent(socketChannel); - try (WritableByteChannel outputChannel = TunnelClient.this.tunnelConnection.open(socketChannel, - closeable)) { - logger.trace( - "Accepted connection to tunnel client from " + socketChannel.socket().getRemoteSocketAddress()); - while (true) { - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); - int amountRead = socketChannel.read(buffer); - if (amountRead == -1) { - return; - } - if (amountRead > 0) { - buffer.flip(); - outputChannel.write(buffer); - } - } - } - } - - protected void stopAcceptingConnections() { - this.acceptConnections = false; - } - - } - - /** - * {@link Closeable} used to close a {@link SocketChannel} and fire an event. - */ - private class SocketCloseable implements Closeable { - - private final SocketChannel socketChannel; - - private boolean closed = false; - - SocketCloseable(SocketChannel socketChannel) { - this.socketChannel = socketChannel; - } - - @Override - public void close() throws IOException { - if (!this.closed) { - this.socketChannel.close(); - TunnelClient.this.listeners.fireCloseEvent(this.socketChannel); - this.closed = true; - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListener.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListener.java deleted file mode 100644 index 78b2e85c68..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.nio.channels.SocketChannel; - -/** - * Listener that can be used to receive {@link TunnelClient} events. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public interface TunnelClientListener { - - /** - * Called when a socket channel is opened. - * @param socket the socket channel - */ - void onOpen(SocketChannel socket); - - /** - * Called when a socket channel is closed. - * @param socket the socket channel - */ - void onClose(SocketChannel socket); - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListeners.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListeners.java deleted file mode 100644 index 699ce900f4..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClientListeners.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.nio.channels.SocketChannel; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.springframework.util.Assert; - -/** - * A collection of {@link TunnelClientListener}. - * - * @author Phillip Webb - */ -class TunnelClientListeners { - - private final List listeners = new CopyOnWriteArrayList<>(); - - void addListener(TunnelClientListener listener) { - Assert.notNull(listener, "Listener must not be null"); - this.listeners.add(listener); - } - - void removeListener(TunnelClientListener listener) { - Assert.notNull(listener, "Listener must not be null"); - this.listeners.remove(listener); - } - - void fireOpenEvent(SocketChannel socket) { - for (TunnelClientListener listener : this.listeners) { - listener.onOpen(socket); - } - } - - void fireCloseEvent(SocketChannel socket) { - for (TunnelClientListener listener : this.listeners) { - listener.onClose(socket); - } - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelConnection.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelConnection.java deleted file mode 100644 index 555ef4730b..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelConnection.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.io.Closeable; -import java.nio.channels.WritableByteChannel; - -/** - * Interface used to manage socket tunnel connections. - * - * @author Phillip Webb - * @since 1.3.0 - */ -@FunctionalInterface -public interface TunnelConnection { - - /** - * Open the tunnel connection. - * @param incomingChannel a {@link WritableByteChannel} that should be used to write - * any incoming data received from the remote server - * @param closeable a closeable to call when the channel is closed - * @return a {@link WritableByteChannel} that should be used to send any outgoing data - * destined for the remote server - * @throws Exception in case of errors - */ - WritableByteChannel open(WritableByteChannel incomingChannel, Closeable closeable) throws Exception; - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/package-info.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/package-info.java deleted file mode 100644 index c60d12df8f..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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. - */ - -/** - * Client side TCP tunnel support. - */ -package org.springframework.boot.devtools.tunnel.client; diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/package-info.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/package-info.java deleted file mode 100644 index 9ce7964471..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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. - */ - -/** - * Provides support for tunneling TCP traffic over HTTP. Tunneling is primarily designed - * for the Java Debug Wire Protocol (JDWP) and as such only expects a single connection - * and isn't particularly worried about resource usage. - */ -package org.springframework.boot.devtools.tunnel; diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayload.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayload.java deleted file mode 100644 index 8d4459fcce..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayload.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.payload; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Encapsulates a payload data sent over a HTTP tunnel. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public class HttpTunnelPayload { - - private static final String SEQ_HEADER = "x-seq"; - - private static final int BUFFER_SIZE = 1024 * 100; - - protected static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); - - private static final Log logger = LogFactory.getLog(HttpTunnelPayload.class); - - private final long sequence; - - private final ByteBuffer data; - - /** - * Create a new {@link HttpTunnelPayload} instance. - * @param sequence the sequence number of the payload - * @param data the payload data - */ - public HttpTunnelPayload(long sequence, ByteBuffer data) { - Assert.isTrue(sequence > 0, "Sequence must be positive"); - Assert.notNull(data, "Data must not be null"); - this.sequence = sequence; - this.data = data; - } - - /** - * Return the sequence number of the payload. - * @return the sequence - */ - public long getSequence() { - return this.sequence; - } - - /** - * Assign this payload to the given {@link HttpOutputMessage}. - * @param message the message to assign this payload to - * @throws IOException in case of I/O errors - */ - public void assignTo(HttpOutputMessage message) throws IOException { - Assert.notNull(message, "Message must not be null"); - HttpHeaders headers = message.getHeaders(); - headers.setContentLength(this.data.remaining()); - headers.add(SEQ_HEADER, Long.toString(getSequence())); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - try (WritableByteChannel body = Channels.newChannel(message.getBody())) { - while (this.data.hasRemaining()) { - body.write(this.data); - } - } - } - - /** - * Write the content of this payload to the given target channel. - * @param channel the channel to write to - * @throws IOException in case of I/O errors - */ - public void writeTo(WritableByteChannel channel) throws IOException { - Assert.notNull(channel, "Channel must not be null"); - while (this.data.hasRemaining()) { - channel.write(this.data); - } - } - - /** - * Return the {@link HttpTunnelPayload} for the given message or {@code null} if there - * is no payload. - * @param message the HTTP message - * @return the payload or {@code null} - * @throws IOException in case of I/O errors - */ - public static HttpTunnelPayload get(HttpInputMessage message) throws IOException { - long length = message.getHeaders().getContentLength(); - if (length <= 0) { - return null; - } - String seqHeader = message.getHeaders().getFirst(SEQ_HEADER); - Assert.state(StringUtils.hasLength(seqHeader), "Missing sequence header"); - try (ReadableByteChannel body = Channels.newChannel(message.getBody())) { - ByteBuffer payload = ByteBuffer.allocate((int) length); - while (payload.hasRemaining()) { - body.read(payload); - } - payload.flip(); - return new HttpTunnelPayload(Long.parseLong(seqHeader), payload); - } - } - - /** - * Return the payload data for the given source {@link ReadableByteChannel} or null if - * the channel timed out whilst reading. - * @param channel the source channel - * @return payload data or {@code null} - * @throws IOException in case of I/O errors - */ - public static ByteBuffer getPayloadData(ReadableByteChannel channel) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); - try { - int amountRead = channel.read(buffer); - Assert.state(amountRead != -1, "Target server connection closed"); - buffer.flip(); - return buffer; - } - catch (InterruptedIOException ex) { - return null; - } - } - - /** - * Log incoming payload information at trace level to aid diagnostics. - */ - public void logIncoming() { - log("< "); - } - - /** - * Log incoming payload information at trace level to aid diagnostics. - */ - public void logOutgoing() { - log("> "); - } - - private void log(String prefix) { - if (logger.isTraceEnabled()) { - logger.trace(prefix + toHexString()); - } - } - - /** - * Return the payload as a hexadecimal string. - * @return the payload as a hex string - */ - public String toHexString() { - byte[] bytes = this.data.array(); - char[] hex = new char[this.data.remaining() * 2]; - for (int i = this.data.position(); i < this.data.remaining(); i++) { - int b = bytes[i] & 0xFF; - hex[i * 2] = HEX_CHARS[b >>> 4]; - hex[i * 2 + 1] = HEX_CHARS[b & 0x0F]; - } - return new String(hex); - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java deleted file mode 100644 index 0bf486fcaa..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.payload; - -import java.io.IOException; -import java.nio.channels.WritableByteChannel; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.util.Assert; - -/** - * Utility class that forwards {@link HttpTunnelPayload} instances to a destination - * channel, respecting sequence order. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public class HttpTunnelPayloadForwarder { - - private static final int MAXIMUM_QUEUE_SIZE = 100; - - private final Map queue = new HashMap<>(); - - private final Object monitor = new Object(); - - private final WritableByteChannel targetChannel; - - private long lastRequestSeq = 0; - - /** - * Create a new {@link HttpTunnelPayloadForwarder} instance. - * @param targetChannel the target channel - */ - public HttpTunnelPayloadForwarder(WritableByteChannel targetChannel) { - Assert.notNull(targetChannel, "TargetChannel must not be null"); - this.targetChannel = targetChannel; - } - - public void forward(HttpTunnelPayload payload) throws IOException { - synchronized (this.monitor) { - long seq = payload.getSequence(); - if (this.lastRequestSeq != seq - 1) { - Assert.state(this.queue.size() < MAXIMUM_QUEUE_SIZE, "Too many messages queued"); - this.queue.put(seq, payload); - return; - } - payload.logOutgoing(); - payload.writeTo(this.targetChannel); - this.lastRequestSeq = seq; - HttpTunnelPayload queuedItem = this.queue.get(seq + 1); - if (queuedItem != null) { - forward(queuedItem); - } - } - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/package-info.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/package-info.java deleted file mode 100644 index e67868b356..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * 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 - * - * https://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. - */ - -/** - * Classes to deal with payloads sent over an HTTP tunnel. - */ -package org.springframework.boot.devtools.tunnel.payload; diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java deleted file mode 100644 index 865946206f..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.IOException; -import java.net.ConnectException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Iterator; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload; -import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.server.ServerHttpAsyncRequestControl; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.util.Assert; - -/** - * A server that can be used to tunnel TCP traffic over HTTP. Similar in design to the - * Bidirectional-streams Over - * Synchronous HTTP (BOSH) XMPP extension protocol, the server uses long polling with - * HTTP requests held open until a response is available. A typical traffic pattern would - * be as follows: - * - *
- * [ CLIENT ]                      [ SERVER ]
- *     | (a) Initial empty request     |
- *     |------------------------------>|
- *     | (b) Data I                    |
- *  -->|------------------------------>|--->
- *     | Response I (a)                |
- *  <--|<------------------------------|<---
- *     |                               |
- *     | (c) Data II                   |
- *  -->|------------------------------>|--->
- *     | Response II (b)               |
- *  <--|<------------------------------|<---
- *     .                               .
- *     .                               .
- * 
- * - * Each incoming request is held open to be used to carry the next available response. The - * server will hold at most two connections open at any given time. - *

- * Requests should be made using HTTP GET or POST (depending if there is a payload), with - * any payload contained in the body. The following response codes can be returned from - * the server: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Response Codes
StatusMeaning
200 (OK)Data payload response.
204 (No Content)The long poll has timed out and the client should start a new request.
429 (Too many requests)There are already enough connections open, this one can be dropped.
410 (Gone)The target server has disconnected.
503 (Service Unavailable)The target server is unavailable
- *

- * Requests and responses that contain payloads include a {@code x-seq} header that - * contains a running sequence number (used to ensure data is applied in the correct - * order). The first request containing a payload should have a {@code x-seq} value of - * {@code 1}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.3.0 - * @see org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection - */ -public class HttpTunnelServer { - - private static final long DEFAULT_LONG_POLL_TIMEOUT = TimeUnit.SECONDS.toMillis(10); - - private static final long DEFAULT_DISCONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(30); - - private static final MediaType DISCONNECT_MEDIA_TYPE = new MediaType("application", "x-disconnect"); - - private static final Log logger = LogFactory.getLog(HttpTunnelServer.class); - - private final TargetServerConnection serverConnection; - - private int longPollTimeout = (int) DEFAULT_LONG_POLL_TIMEOUT; - - private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT; - - private volatile ServerThread serverThread; - - /** - * Creates a new {@link HttpTunnelServer} instance. - * @param serverConnection the connection to the target server - */ - public HttpTunnelServer(TargetServerConnection serverConnection) { - Assert.notNull(serverConnection, "ServerConnection must not be null"); - this.serverConnection = serverConnection; - } - - /** - * Handle an incoming HTTP connection. - * @param request the HTTP request - * @param response the HTTP response - * @throws IOException in case of I/O errors - */ - public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException { - handle(new HttpConnection(request, response)); - } - - /** - * Handle an incoming HTTP connection. - * @param httpConnection the HTTP connection - * @throws IOException in case of I/O errors - */ - protected void handle(HttpConnection httpConnection) throws IOException { - try { - getServerThread().handleIncomingHttp(httpConnection); - httpConnection.waitForResponse(); - } - catch (ConnectException ex) { - httpConnection.respond(HttpStatus.GONE); - } - } - - /** - * Returns the active server thread, creating and starting it if necessary. - * @return the {@code ServerThread} (never {@code null}) - * @throws IOException in case of I/O errors - */ - protected ServerThread getServerThread() throws IOException { - synchronized (this) { - if (this.serverThread == null) { - ByteChannel channel = this.serverConnection.open(this.longPollTimeout); - this.serverThread = new ServerThread(channel); - this.serverThread.start(); - } - return this.serverThread; - } - } - - /** - * Called when the server thread exits. - */ - void clearServerThread() { - synchronized (this) { - this.serverThread = null; - } - } - - /** - * Set the long poll timeout for the server. - * @param longPollTimeout the long poll timeout in milliseconds - */ - public void setLongPollTimeout(int longPollTimeout) { - Assert.isTrue(longPollTimeout > 0, "LongPollTimeout must be a positive value"); - this.longPollTimeout = longPollTimeout; - } - - /** - * Set the maximum amount of time to wait for a client before closing the connection. - * @param disconnectTimeout the disconnect timeout in milliseconds - */ - public void setDisconnectTimeout(long disconnectTimeout) { - Assert.isTrue(disconnectTimeout > 0, "DisconnectTimeout must be a positive value"); - this.disconnectTimeout = disconnectTimeout; - } - - /** - * The main server thread used to transfer tunnel traffic. - */ - protected class ServerThread extends Thread { - - private final ByteChannel targetServer; - - private final Deque httpConnections; - - private final HttpTunnelPayloadForwarder payloadForwarder; - - private boolean closed; - - private AtomicLong responseSeq = new AtomicLong(); - - private long lastHttpRequestTime; - - public ServerThread(ByteChannel targetServer) { - Assert.notNull(targetServer, "TargetServer must not be null"); - this.targetServer = targetServer; - this.httpConnections = new ArrayDeque<>(2); - this.payloadForwarder = new HttpTunnelPayloadForwarder(targetServer); - } - - @Override - public void run() { - try { - try { - readAndForwardTargetServerData(); - } - catch (Exception ex) { - logger.trace("Unexpected exception from tunnel server", ex); - } - } - finally { - this.closed = true; - closeHttpConnections(); - closeTargetServer(); - HttpTunnelServer.this.clearServerThread(); - } - } - - private void readAndForwardTargetServerData() throws IOException { - while (this.targetServer.isOpen()) { - closeStaleHttpConnections(); - ByteBuffer data = HttpTunnelPayload.getPayloadData(this.targetServer); - synchronized (this.httpConnections) { - if (data != null) { - HttpTunnelPayload payload = new HttpTunnelPayload(this.responseSeq.incrementAndGet(), data); - payload.logIncoming(); - HttpConnection connection = getOrWaitForHttpConnection(); - connection.respond(payload); - } - } - } - } - - private HttpConnection getOrWaitForHttpConnection() { - synchronized (this.httpConnections) { - HttpConnection httpConnection = this.httpConnections.pollFirst(); - while (httpConnection == null) { - try { - this.httpConnections.wait(HttpTunnelServer.this.longPollTimeout); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - closeHttpConnections(); - } - httpConnection = this.httpConnections.pollFirst(); - } - return httpConnection; - } - } - - private void closeStaleHttpConnections() throws IOException { - synchronized (this.httpConnections) { - checkNotDisconnected(); - Iterator iterator = this.httpConnections.iterator(); - while (iterator.hasNext()) { - HttpConnection httpConnection = iterator.next(); - if (httpConnection.isOlderThan(HttpTunnelServer.this.longPollTimeout)) { - httpConnection.respond(HttpStatus.NO_CONTENT); - iterator.remove(); - } - } - } - } - - private void checkNotDisconnected() { - if (this.lastHttpRequestTime > 0) { - long timeout = HttpTunnelServer.this.disconnectTimeout; - long duration = System.currentTimeMillis() - this.lastHttpRequestTime; - Assert.state(duration < timeout, () -> "Disconnect timeout: " + timeout + " " + duration); - } - } - - private void closeHttpConnections() { - synchronized (this.httpConnections) { - while (!this.httpConnections.isEmpty()) { - try { - this.httpConnections.removeFirst().respond(HttpStatus.GONE); - } - catch (Exception ex) { - logger.trace("Unable to close remote HTTP connection"); - } - } - } - } - - private void closeTargetServer() { - try { - this.targetServer.close(); - } - catch (IOException ex) { - logger.trace("Unable to target server connection"); - } - } - - /** - * Handle an incoming {@link HttpConnection}. - * @param httpConnection the connection to handle. - * @throws IOException in case of I/O errors - */ - public void handleIncomingHttp(HttpConnection httpConnection) throws IOException { - if (this.closed) { - httpConnection.respond(HttpStatus.GONE); - } - synchronized (this.httpConnections) { - while (this.httpConnections.size() > 1) { - this.httpConnections.removeFirst().respond(HttpStatus.TOO_MANY_REQUESTS); - } - this.lastHttpRequestTime = System.currentTimeMillis(); - this.httpConnections.addLast(httpConnection); - this.httpConnections.notify(); - } - forwardToTargetServer(httpConnection); - } - - private void forwardToTargetServer(HttpConnection httpConnection) throws IOException { - if (httpConnection.isDisconnectRequest()) { - this.targetServer.close(); - interrupt(); - } - ServerHttpRequest request = httpConnection.getRequest(); - HttpTunnelPayload payload = HttpTunnelPayload.get(request); - if (payload != null) { - this.payloadForwarder.forward(payload); - } - } - - } - - /** - * Encapsulates an HTTP request/response pair. - */ - protected static class HttpConnection { - - private final long createTime; - - private final ServerHttpRequest request; - - private final ServerHttpResponse response; - - private ServerHttpAsyncRequestControl async; - - private volatile boolean complete = false; - - public HttpConnection(ServerHttpRequest request, ServerHttpResponse response) { - this.createTime = System.currentTimeMillis(); - this.request = request; - this.response = response; - this.async = startAsync(); - } - - /** - * Start asynchronous support or if unavailable return {@code null} to cause - * {@link #waitForResponse()} to block. - * @return the async request control - */ - protected ServerHttpAsyncRequestControl startAsync() { - try { - // Try to use async to save blocking - ServerHttpAsyncRequestControl async = this.request.getAsyncRequestControl(this.response); - async.start(); - return async; - } - catch (Exception ex) { - return null; - } - } - - /** - * Return the underlying request. - * @return the request - */ - public final ServerHttpRequest getRequest() { - return this.request; - } - - /** - * Return the underlying response. - * @return the response - */ - protected final ServerHttpResponse getResponse() { - return this.response; - } - - /** - * Determine if a connection is older than the specified time. - * @param time the time to check - * @return {@code true} if the request is older than the time - */ - public boolean isOlderThan(int time) { - long runningTime = System.currentTimeMillis() - this.createTime; - return (runningTime > time); - } - - /** - * Cause the request to block or use asynchronous methods to wait until a response - * is available. - */ - public void waitForResponse() { - if (this.async == null) { - while (!this.complete) { - try { - synchronized (this) { - wait(1000); - } - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } - } - - /** - * Detect if the request is actually a signal to disconnect. - * @return if the request is a signal to disconnect - */ - public boolean isDisconnectRequest() { - return DISCONNECT_MEDIA_TYPE.equals(this.request.getHeaders().getContentType()); - } - - /** - * Send an HTTP status response. - * @param status the status to send - * @throws IOException in case of I/O errors - */ - public void respond(HttpStatus status) throws IOException { - Assert.notNull(status, "Status must not be null"); - this.response.setStatusCode(status); - complete(); - } - - /** - * Send a payload response. - * @param payload the payload to send - * @throws IOException in case of I/O errors - */ - public void respond(HttpTunnelPayload payload) throws IOException { - Assert.notNull(payload, "Payload must not be null"); - this.response.setStatusCode(HttpStatus.OK); - payload.assignTo(this.response); - complete(); - } - - /** - * Called when a request is complete. - */ - protected void complete() { - if (this.async != null) { - this.async.complete(); - } - else { - synchronized (this) { - this.complete = true; - notifyAll(); - } - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandler.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandler.java deleted file mode 100644 index 1ca001a141..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.IOException; - -import org.springframework.boot.devtools.remote.server.Handler; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.util.Assert; - -/** - * Adapts a {@link HttpTunnelServer} to a {@link Handler}. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public class HttpTunnelServerHandler implements Handler { - - private HttpTunnelServer server; - - /** - * Create a new {@link HttpTunnelServerHandler} instance. - * @param server the server to adapt - */ - public HttpTunnelServerHandler(HttpTunnelServer server) { - Assert.notNull(server, "Server must not be null"); - this.server = server; - } - - @Override - public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException { - this.server.handle(request, response); - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/PortProvider.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/PortProvider.java deleted file mode 100644 index 80ac4a2076..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/PortProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -/** - * Strategy interface to provide access to a port (which may change if an existing - * connection is closed). - * - * @author Phillip Webb - * @since 1.3.0 - */ -@FunctionalInterface -public interface PortProvider { - - /** - * Return the port number. - * @return the port number - */ - int getPort(); - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnection.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnection.java deleted file mode 100644 index 78aac9c1f8..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnection.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.SocketChannel; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.log.LogMessage; -import org.springframework.util.Assert; - -/** - * Socket based {@link TargetServerConnection}. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public class SocketTargetServerConnection implements TargetServerConnection { - - private static final Log logger = LogFactory.getLog(SocketTargetServerConnection.class); - - private final PortProvider portProvider; - - /** - * Create a new {@link SocketTargetServerConnection}. - * @param portProvider the port provider - */ - public SocketTargetServerConnection(PortProvider portProvider) { - Assert.notNull(portProvider, "PortProvider must not be null"); - this.portProvider = portProvider; - } - - @Override - public ByteChannel open(int socketTimeout) throws IOException { - SocketAddress address = new InetSocketAddress(this.portProvider.getPort()); - logger.trace(LogMessage.format("Opening tunnel connection to target server on %s", address)); - SocketChannel channel = SocketChannel.open(address); - channel.socket().setSoTimeout(socketTimeout); - return new TimeoutAwareChannel(channel); - } - - /** - * Wrapper to expose the {@link SocketChannel} in such a way that - * {@code SocketTimeoutExceptions} are still thrown from read methods. - */ - private static class TimeoutAwareChannel implements ByteChannel { - - private final SocketChannel socketChannel; - - private final ReadableByteChannel readChannel; - - TimeoutAwareChannel(SocketChannel socketChannel) throws IOException { - this.socketChannel = socketChannel; - this.readChannel = Channels.newChannel(socketChannel.socket().getInputStream()); - } - - @Override - public int read(ByteBuffer dst) throws IOException { - return this.readChannel.read(dst); - } - - @Override - public int write(ByteBuffer src) throws IOException { - return this.socketChannel.write(src); - } - - @Override - public boolean isOpen() { - return this.socketChannel.isOpen(); - } - - @Override - public void close() throws IOException { - this.socketChannel.close(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/StaticPortProvider.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/StaticPortProvider.java deleted file mode 100644 index 599ecb47bc..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/StaticPortProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import org.springframework.util.Assert; - -/** - * {@link PortProvider} for a static port that won't change. - * - * @author Phillip Webb - * @since 1.3.0 - */ -public class StaticPortProvider implements PortProvider { - - private final int port; - - public StaticPortProvider(int port) { - Assert.isTrue(port > 0, "Port must be positive"); - this.port = port; - } - - @Override - public int getPort() { - return this.port; - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/TargetServerConnection.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/TargetServerConnection.java deleted file mode 100644 index 4fcf7dd83c..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/TargetServerConnection.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.IOException; -import java.nio.channels.ByteChannel; - -/** - * Manages the connection to the ultimate tunnel target server. - * - * @author Phillip Webb - * @since 1.3.0 - */ -@FunctionalInterface -public interface TargetServerConnection { - - /** - * Open a connection to the target server with the specified timeout. - * @param timeout the read timeout - * @return a {@link ByteChannel} providing read/write access to the server - * @throws IOException in case of I/O errors - */ - ByteChannel open(int timeout) throws IOException; - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/package-info.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/package-info.java deleted file mode 100644 index 38e55f0260..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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. - */ - -/** - * Server side TCP tunnel support. - */ -package org.springframework.boot.devtools.tunnel.server; diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/integrationtest/HttpTunnelIntegrationTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/integrationtest/HttpTunnelIntegrationTests.java deleted file mode 100644 index e55734367e..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/integrationtest/HttpTunnelIntegrationTests.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.integrationtest; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.devtools.integrationtest.HttpTunnelIntegrationTests.TunnelConfiguration.TestTunnelClient; -import org.springframework.boot.devtools.remote.server.AccessManager; -import org.springframework.boot.devtools.remote.server.Dispatcher; -import org.springframework.boot.devtools.remote.server.DispatcherFilter; -import org.springframework.boot.devtools.remote.server.HandlerMapper; -import org.springframework.boot.devtools.remote.server.UrlHandlerMapper; -import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection; -import org.springframework.boot.devtools.tunnel.client.TunnelClient; -import org.springframework.boot.devtools.tunnel.client.TunnelConnection; -import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer; -import org.springframework.boot.devtools.tunnel.server.HttpTunnelServerHandler; -import org.springframework.boot.devtools.tunnel.server.SocketTargetServerConnection; -import org.springframework.boot.devtools.tunnel.server.TargetServerConnection; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Simple integration tests for HTTP tunneling. - * - * @author Phillip Webb - */ -class HttpTunnelIntegrationTests { - - @Test - void httpServerDirect() { - AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); - context.register(ServerConfiguration.class); - context.refresh(); - String url = "http://localhost:" + context.getWebServer().getPort() + "/hello"; - ResponseEntity entity = new TestRestTemplate().getForEntity(url, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); - context.close(); - } - - @Test - void viaTunnel() { - AnnotationConfigServletWebServerApplicationContext serverContext = new AnnotationConfigServletWebServerApplicationContext(); - serverContext.register(ServerConfiguration.class); - serverContext.refresh(); - AnnotationConfigApplicationContext tunnelContext = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("server.port:" + serverContext.getWebServer().getPort()).applyTo(tunnelContext); - tunnelContext.register(TunnelConfiguration.class); - tunnelContext.refresh(); - String url = "http://localhost:" + tunnelContext.getBean(TestTunnelClient.class).port + "/hello"; - ResponseEntity entity = new TestRestTemplate().getForEntity(url, String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("Hello World"); - serverContext.close(); - tunnelContext.close(); - } - - @Configuration(proxyBeanMethods = false) - @EnableWebMvc - static class ServerConfiguration { - - @Bean - ServletWebServerFactory container() { - return new TomcatServletWebServerFactory(0); - } - - @Bean - DispatcherServlet dispatcherServlet() { - return new DispatcherServlet(); - } - - @Bean - MyController myController() { - return new MyController(); - } - - @Bean - DispatcherFilter filter(AnnotationConfigServletWebServerApplicationContext context) { - TargetServerConnection connection = new SocketTargetServerConnection( - () -> context.getWebServer().getPort()); - HttpTunnelServer server = new HttpTunnelServer(connection); - HandlerMapper mapper = new UrlHandlerMapper("/httptunnel", new HttpTunnelServerHandler(server)); - Collection mappers = Collections.singleton(mapper); - Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers); - return new DispatcherFilter(dispatcher); - } - - } - - @org.springframework.context.annotation.Configuration(proxyBeanMethods = false) - static class TunnelConfiguration { - - @Bean - TunnelClient tunnelClient(@Value("${server.port}") int serverPort) { - String url = "http://localhost:" + serverPort + "/httptunnel"; - TunnelConnection connection = new HttpTunnelConnection(url, new SimpleClientHttpRequestFactory()); - return new TestTunnelClient(0, connection); - } - - static class TestTunnelClient extends TunnelClient { - - private int port; - - TestTunnelClient(int listenPort, TunnelConnection tunnelConnection) { - super(listenPort, tunnelConnection); - } - - @Override - public int start() throws IOException { - this.port = super.start(); - return this.port; - } - - } - - } - - @RestController - static class MyController { - - @RequestMapping("/hello") - String hello() { - return "Hello World"; - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnectionTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnectionTests.java deleted file mode 100644 index b1995cda93..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnectionTests.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.net.ConnectException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.util.concurrent.Executor; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.boot.devtools.test.MockClientHttpRequestFactory; -import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection.TunnelChannel; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.http.HttpStatus; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.never; - -/** - * Tests for {@link HttpTunnelConnection}. - * - * @author Phillip Webb - * @author Rob Winch - * @author Andy Wilkinson - */ -@ExtendWith({ OutputCaptureExtension.class, MockitoExtension.class }) -class HttpTunnelConnectionTests { - - private String url; - - private ByteArrayOutputStream incomingData; - - private WritableByteChannel incomingChannel; - - @Mock - private Closeable closeable; - - private MockClientHttpRequestFactory requestFactory = new MockClientHttpRequestFactory(); - - @BeforeEach - void setup() { - this.url = "http://localhost:12345"; - this.incomingData = new ByteArrayOutputStream(); - this.incomingChannel = Channels.newChannel(this.incomingData); - } - - @Test - void urlMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelConnection(null, this.requestFactory)) - .withMessageContaining("URL must not be empty"); - } - - @Test - void urlMustNotBeEmpty() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelConnection("", this.requestFactory)) - .withMessageContaining("URL must not be empty"); - } - - @Test - void urlMustNotBeMalformed() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpTunnelConnection("htttttp:///ttest", this.requestFactory)) - .withMessageContaining("Malformed URL 'htttttp:///ttest'"); - } - - @Test - void requestFactoryMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelConnection(this.url, null)) - .withMessageContaining("RequestFactory must not be null"); - } - - @Test - void closeTunnelChangesIsOpen() throws Exception { - this.requestFactory.willRespondAfterDelay(1000, HttpStatus.GONE); - WritableByteChannel channel = openTunnel(false); - assertThat(channel.isOpen()).isTrue(); - channel.close(); - assertThat(channel.isOpen()).isFalse(); - } - - @Test - void closeTunnelCallsCloseableOnce() throws Exception { - this.requestFactory.willRespondAfterDelay(1000, HttpStatus.GONE); - WritableByteChannel channel = openTunnel(false); - then(this.closeable).should(never()).close(); - channel.close(); - channel.close(); - then(this.closeable).should().close(); - } - - @Test - void typicalTraffic() throws Exception { - this.requestFactory.willRespond("hi", "=2", "=3"); - TunnelChannel channel = openTunnel(true); - write(channel, "hello"); - write(channel, "1+1"); - write(channel, "1+2"); - assertThat(this.incomingData.toString()).isEqualTo("hi=2=3"); - } - - @Test - void trafficWithLongPollTimeouts() throws Exception { - for (int i = 0; i < 10; i++) { - this.requestFactory.willRespond(HttpStatus.NO_CONTENT); - } - this.requestFactory.willRespond("hi"); - TunnelChannel channel = openTunnel(true); - write(channel, "hello"); - assertThat(this.incomingData.toString()).isEqualTo("hi"); - assertThat(this.requestFactory.getExecutedRequests().size()).isGreaterThan(10); - } - - @Test - void connectFailureLogsWarning(CapturedOutput output) throws Exception { - this.requestFactory.willRespond(new ConnectException()); - TunnelChannel tunnel = openTunnel(true); - assertThat(tunnel.isOpen()).isFalse(); - assertThat(output).contains("Failed to connect to remote application at http://localhost:12345"); - } - - private void write(TunnelChannel channel, String string) throws IOException { - channel.write(ByteBuffer.wrap(string.getBytes())); - } - - private TunnelChannel openTunnel(boolean singleThreaded) throws Exception { - HttpTunnelConnection connection = new HttpTunnelConnection(this.url, this.requestFactory, - singleThreaded ? new CurrentThreadExecutor() : null); - return connection.open(this.incomingChannel, this.closeable); - } - - static class CurrentThreadExecutor implements Executor { - - @Override - public void execute(Runnable command) { - command.run(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/TunnelClientTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/TunnelClientTests.java deleted file mode 100644 index 7968f674a4..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/TunnelClientTests.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.client; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.SocketChannel; -import java.nio.channels.WritableByteChannel; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; - -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link TunnelClient}. - * - * @author Phillip Webb - */ -class TunnelClientTests { - - private MockTunnelConnection tunnelConnection = new MockTunnelConnection(); - - @Test - void listenPortMustNotBeNegative() { - assertThatIllegalArgumentException().isThrownBy(() -> new TunnelClient(-5, this.tunnelConnection)) - .withMessageContaining("ListenPort must be greater than or equal to 0"); - } - - @Test - void tunnelConnectionMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new TunnelClient(1, null)) - .withMessageContaining("TunnelConnection must not be null"); - } - - @Test - void typicalTraffic() throws Exception { - TunnelClient client = new TunnelClient(0, this.tunnelConnection); - int port = client.start(); - SocketChannel channel = SocketChannel.open(new InetSocketAddress(port)); - channel.write(ByteBuffer.wrap("hello".getBytes())); - ByteBuffer buffer = ByteBuffer.allocate(5); - channel.read(buffer); - channel.close(); - this.tunnelConnection.verifyWritten("hello"); - assertThat(new String(buffer.array())).isEqualTo("olleh"); - } - - @Test - void socketChannelClosedTriggersTunnelClose() throws Exception { - TunnelClient client = new TunnelClient(0, this.tunnelConnection); - int port = client.start(); - SocketChannel channel = SocketChannel.open(new InetSocketAddress(port)); - Awaitility.await() - .atMost(Duration.ofSeconds(30)) - .until(this.tunnelConnection::getOpenedTimes, (open) -> open == 1); - channel.close(); - client.getServerThread().stopAcceptingConnections(); - client.getServerThread().join(2000); - assertThat(this.tunnelConnection.getOpenedTimes()).isEqualTo(1); - assertThat(this.tunnelConnection.isOpen()).isFalse(); - } - - @Test - void stopTriggersTunnelClose() throws Exception { - TunnelClient client = new TunnelClient(0, this.tunnelConnection); - int port = client.start(); - SocketChannel channel = SocketChannel.open(new InetSocketAddress(port)); - Awaitility.await() - .atMost(Duration.ofSeconds(30)) - .until(this.tunnelConnection::getOpenedTimes, (times) -> times == 1); - assertThat(this.tunnelConnection.isOpen()).isTrue(); - client.stop(); - assertThat(this.tunnelConnection.isOpen()).isFalse(); - assertThat(readWithPossibleFailure(channel)).satisfiesAnyOf((result) -> assertThat(result).isEqualTo(-1), - (result) -> assertThat(result).isInstanceOf(SocketException.class)); - } - - private Object readWithPossibleFailure(SocketChannel channel) { - try { - return channel.read(ByteBuffer.allocate(1)); - } - catch (Exception ex) { - return ex; - } - } - - @Test - void addListener() throws Exception { - TunnelClient client = new TunnelClient(0, this.tunnelConnection); - MockTunnelClientListener listener = new MockTunnelClientListener(); - client.addListener(listener); - int port = client.start(); - SocketChannel channel = SocketChannel.open(new InetSocketAddress(port)); - Awaitility.await().atMost(Duration.ofSeconds(30)).until(listener.onOpen::get, (open) -> open == 1); - assertThat(listener.onClose).hasValue(0); - client.getServerThread().stopAcceptingConnections(); - channel.close(); - Awaitility.await().atMost(Duration.ofSeconds(30)).until(listener.onClose::get, (close) -> close == 1); - client.getServerThread().join(2000); - } - - static class MockTunnelClientListener implements TunnelClientListener { - - private final AtomicInteger onOpen = new AtomicInteger(); - - private final AtomicInteger onClose = new AtomicInteger(); - - @Override - public void onOpen(SocketChannel socket) { - this.onOpen.incrementAndGet(); - } - - @Override - public void onClose(SocketChannel socket) { - this.onClose.incrementAndGet(); - } - - } - - static class MockTunnelConnection implements TunnelConnection { - - private final ByteArrayOutputStream written = new ByteArrayOutputStream(); - - private boolean open; - - private int openedTimes; - - @Override - public WritableByteChannel open(WritableByteChannel incomingChannel, Closeable closeable) { - this.openedTimes++; - this.open = true; - return new TunnelChannel(incomingChannel, closeable); - } - - void verifyWritten(String expected) { - verifyWritten(expected.getBytes()); - } - - void verifyWritten(byte[] expected) { - synchronized (this.written) { - assertThat(this.written.toByteArray()).isEqualTo(expected); - this.written.reset(); - } - } - - boolean isOpen() { - return this.open; - } - - int getOpenedTimes() { - return this.openedTimes; - } - - private class TunnelChannel implements WritableByteChannel { - - private final WritableByteChannel incomingChannel; - - private final Closeable closeable; - - TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) { - this.incomingChannel = incomingChannel; - this.closeable = closeable; - } - - @Override - public boolean isOpen() { - return MockTunnelConnection.this.open; - } - - @Override - public void close() throws IOException { - MockTunnelConnection.this.open = false; - this.closeable.close(); - } - - @Override - public int write(ByteBuffer src) throws IOException { - int remaining = src.remaining(); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Channels.newChannel(stream).write(src); - byte[] bytes = stream.toByteArray(); - synchronized (MockTunnelConnection.this.written) { - MockTunnelConnection.this.written.write(bytes); - } - byte[] reversed = new byte[bytes.length]; - for (int i = 0; i < reversed.length; i++) { - reversed[i] = bytes[bytes.length - 1 - i]; - } - this.incomingChannel.write(ByteBuffer.wrap(reversed)); - return remaining; - } - - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarderTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarderTests.java deleted file mode 100644 index 5907ee1845..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarderTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.payload; - -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -/** - * Tests for {@link HttpTunnelPayloadForwarder}. - * - * @author Phillip Webb - */ -class HttpTunnelPayloadForwarderTests { - - @Test - void targetChannelMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelPayloadForwarder(null)) - .withMessageContaining("TargetChannel must not be null"); - } - - @Test - void forwardInSequence() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - WritableByteChannel channel = Channels.newChannel(out); - HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel); - forwarder.forward(payload(1, "he")); - forwarder.forward(payload(2, "ll")); - forwarder.forward(payload(3, "o")); - assertThat(out.toByteArray()).isEqualTo("hello".getBytes()); - } - - @Test - void forwardOutOfSequence() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - WritableByteChannel channel = Channels.newChannel(out); - HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel); - forwarder.forward(payload(3, "o")); - forwarder.forward(payload(2, "ll")); - forwarder.forward(payload(1, "he")); - assertThat(out.toByteArray()).isEqualTo("hello".getBytes()); - } - - @Test - void overflow() { - WritableByteChannel channel = Channels.newChannel(new ByteArrayOutputStream()); - HttpTunnelPayloadForwarder forwarder = new HttpTunnelPayloadForwarder(channel); - assertThatIllegalStateException().isThrownBy(() -> { - for (int i = 2; i < 130; i++) { - forwarder.forward(payload(i, "data" + i)); - } - }).withMessageContaining("Too many messages queued"); - } - - private HttpTunnelPayload payload(long sequence, String data) { - return new HttpTunnelPayload(sequence, ByteBuffer.wrap(data.getBytes())); - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadTests.java deleted file mode 100644 index a65312f6e4..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadTests.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.payload; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; - -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HttpTunnelPayload}. - * - * @author Phillip Webb - */ -class HttpTunnelPayloadTests { - - @Test - void sequenceMustBePositive() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelPayload(0, ByteBuffer.allocate(1))) - .withMessageContaining("Sequence must be positive"); - } - - @Test - void dataMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelPayload(1, null)) - .withMessageContaining("Data must not be null"); - } - - @Test - void getSequence() { - HttpTunnelPayload payload = new HttpTunnelPayload(1, ByteBuffer.allocate(1)); - assertThat(payload.getSequence()).isEqualTo(1L); - } - - @Test - void getData() throws Exception { - ByteBuffer data = ByteBuffer.wrap("hello".getBytes()); - HttpTunnelPayload payload = new HttpTunnelPayload(1, data); - assertThat(getData(payload)).isEqualTo(data.array()); - } - - @Test - void assignTo() throws Exception { - ByteBuffer data = ByteBuffer.wrap("hello".getBytes()); - HttpTunnelPayload payload = new HttpTunnelPayload(2, data); - MockHttpServletResponse servletResponse = new MockHttpServletResponse(); - HttpOutputMessage response = new ServletServerHttpResponse(servletResponse); - payload.assignTo(response); - assertThat(servletResponse.getHeader("x-seq")).isEqualTo("2"); - assertThat(servletResponse.getContentAsString()).isEqualTo("hello"); - } - - @Test - void getNoData() throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - HttpInputMessage request = new ServletServerHttpRequest(servletRequest); - HttpTunnelPayload payload = HttpTunnelPayload.get(request); - assertThat(payload).isNull(); - } - - @Test - void getWithMissingHeader() { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - servletRequest.setContent("hello".getBytes()); - HttpInputMessage request = new ServletServerHttpRequest(servletRequest); - assertThatIllegalStateException().isThrownBy(() -> HttpTunnelPayload.get(request)) - .withMessageContaining("Missing sequence header"); - } - - @Test - void getWithData() throws Exception { - MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - servletRequest.setContent("hello".getBytes()); - servletRequest.addHeader("x-seq", 123); - HttpInputMessage request = new ServletServerHttpRequest(servletRequest); - HttpTunnelPayload payload = HttpTunnelPayload.get(request); - assertThat(payload.getSequence()).isEqualTo(123L); - assertThat(getData(payload)).isEqualTo("hello".getBytes()); - } - - @Test - void getPayloadData() throws Exception { - ReadableByteChannel channel = Channels.newChannel(new ByteArrayInputStream("hello".getBytes())); - ByteBuffer payloadData = HttpTunnelPayload.getPayloadData(channel); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - WritableByteChannel writeChannel = Channels.newChannel(out); - while (payloadData.hasRemaining()) { - writeChannel.write(payloadData); - } - assertThat(out.toByteArray()).isEqualTo("hello".getBytes()); - } - - @Test - void getPayloadDataWithTimeout() throws Exception { - ReadableByteChannel channel = mock(ReadableByteChannel.class); - given(channel.read(any(ByteBuffer.class))).willThrow(new SocketTimeoutException()); - ByteBuffer payload = HttpTunnelPayload.getPayloadData(channel); - assertThat(payload).isNull(); - } - - private byte[] getData(HttpTunnelPayload payload) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - WritableByteChannel channel = Channels.newChannel(out); - payload.writeTo(channel); - return out.toByteArray(); - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandlerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandlerTests.java deleted file mode 100644 index b046248393..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerHandlerTests.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import org.junit.jupiter.api.Test; - -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; - -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HttpTunnelServerHandler}. - * - * @author Phillip Webb - */ -class HttpTunnelServerHandlerTests { - - @Test - void serverMustNotBeNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelServerHandler(null)) - .withMessageContaining("Server must not be null"); - } - - @Test - void handleDelegatesToServer() throws Exception { - HttpTunnelServer server = mock(HttpTunnelServer.class); - HttpTunnelServerHandler handler = new HttpTunnelServerHandler(server); - ServerHttpRequest request = mock(ServerHttpRequest.class); - ServerHttpResponse response = mock(ServerHttpResponse.class); - handler.handle(request, response); - then(server).should().handle(request, response); - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerTests.java deleted file mode 100644 index a4f64d07f5..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServerTests.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.Channels; -import java.time.Duration; -import java.util.concurrent.BlockingDeque; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.awaitility.Awaitility; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload; -import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer.HttpConnection; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.ServerHttpAsyncRequestControl; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; - -/** - * Tests for {@link HttpTunnelServer}. - * - * @author Phillip Webb - */ -@ExtendWith(MockitoExtension.class) -class HttpTunnelServerTests { - - private static final int DEFAULT_LONG_POLL_TIMEOUT = 10000; - - private static final int JOIN_TIMEOUT = 5000; - - private static final byte[] NO_DATA = {}; - - private static final String SEQ_HEADER = "x-seq"; - - private HttpTunnelServer server; - - @Mock - private TargetServerConnection serverConnection; - - private MockHttpServletRequest servletRequest; - - private MockHttpServletResponse servletResponse; - - private ServerHttpRequest request; - - private ServerHttpResponse response; - - private MockServerChannel serverChannel; - - @BeforeEach - void setup() { - this.server = new HttpTunnelServer(this.serverConnection); - this.servletRequest = new MockHttpServletRequest(); - this.servletRequest.setAsyncSupported(true); - this.servletResponse = new MockHttpServletResponse(); - this.request = new ServletServerHttpRequest(this.servletRequest); - this.response = new ServletServerHttpResponse(this.servletResponse); - this.serverChannel = new MockServerChannel(); - } - - @Test - void serverConnectionIsRequired() { - assertThatIllegalArgumentException().isThrownBy(() -> new HttpTunnelServer(null)) - .withMessageContaining("ServerConnection must not be null"); - } - - @Test - void serverConnectedOnFirstRequest() throws Exception { - then(this.serverConnection).should(never()).open(anyInt()); - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.server.handle(this.request, this.response); - then(this.serverConnection).should().open(DEFAULT_LONG_POLL_TIMEOUT); - } - - @Test - void longPollTimeout() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.server.setLongPollTimeout(800); - this.server.handle(this.request, this.response); - then(this.serverConnection).should().open(800); - } - - @Test - void longPollTimeoutMustBePositiveValue() { - assertThatIllegalArgumentException().isThrownBy(() -> this.server.setLongPollTimeout(0)) - .withMessageContaining("LongPollTimeout must be a positive value"); - } - - @Test - void initialRequestIsSentToServer() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.servletRequest.addHeader(SEQ_HEADER, "1"); - this.servletRequest.setContent("hello".getBytes()); - this.server.handle(this.request, this.response); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - this.serverChannel.verifyReceived("hello"); - } - - @Test - void initialRequestIsUsedForFirstServerResponse() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.servletRequest.addHeader(SEQ_HEADER, "1"); - this.servletRequest.setContent("hello".getBytes()); - this.server.handle(this.request, this.response); - System.out.println("sending"); - this.serverChannel.send("hello"); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - assertThat(this.servletResponse.getContentAsString()).isEqualTo("hello"); - this.serverChannel.verifyReceived("hello"); - } - - @Test - void initialRequestHasNoPayload() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.server.handle(this.request, this.response); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - this.serverChannel.verifyReceived(NO_DATA); - } - - @Test - void typicalRequestResponseTraffic() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - MockHttpConnection h1 = new MockHttpConnection(); - this.server.handle(h1); - MockHttpConnection h2 = new MockHttpConnection("hello server", 1); - this.server.handle(h2); - this.serverChannel.verifyReceived("hello server"); - this.serverChannel.send("hello client"); - h1.verifyReceived("hello client", 1); - MockHttpConnection h3 = new MockHttpConnection("1+1", 2); - this.server.handle(h3); - this.serverChannel.send("=2"); - h2.verifyReceived("=2", 2); - MockHttpConnection h4 = new MockHttpConnection("1+2", 3); - this.server.handle(h4); - this.serverChannel.send("=3"); - h3.verifyReceived("=3", 3); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - } - - @Test - void clientIsAwareOfServerClose() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - MockHttpConnection h1 = new MockHttpConnection("1", 1); - this.server.handle(h1); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - assertThat(h1.getServletResponse().getStatus()).isEqualTo(410); - } - - @Test - void clientCanCloseServer() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - MockHttpConnection h1 = new MockHttpConnection(); - this.server.handle(h1); - MockHttpConnection h2 = new MockHttpConnection("DISCONNECT", 1); - h2.getServletRequest().addHeader("Content-Type", "application/x-disconnect"); - this.server.handle(h2); - this.server.getServerThread().join(JOIN_TIMEOUT); - assertThat(h1.getServletResponse().getStatus()).isEqualTo(410); - assertThat(this.serverChannel.isOpen()).isFalse(); - } - - @Test - void neverMoreThanTwoHttpConnections() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - MockHttpConnection h1 = new MockHttpConnection(); - this.server.handle(h1); - MockHttpConnection h2 = new MockHttpConnection("1", 2); - this.server.handle(h2); - MockHttpConnection h3 = new MockHttpConnection("2", 3); - this.server.handle(h3); - h1.waitForResponse(); - assertThat(h1.getServletResponse().getStatus()).isEqualTo(429); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - } - - @Test - void requestReceivedOutOfOrder() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - MockHttpConnection h1 = new MockHttpConnection(); - MockHttpConnection h2 = new MockHttpConnection("1+2", 1); - MockHttpConnection h3 = new MockHttpConnection("+3", 2); - this.server.handle(h1); - this.server.handle(h3); - this.server.handle(h2); - this.serverChannel.verifyReceived("1+2+3"); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - } - - @Test - void httpConnectionsAreClosedAfterLongPollTimeout() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.server.setDisconnectTimeout(1000); - this.server.setLongPollTimeout(100); - MockHttpConnection h1 = new MockHttpConnection(); - this.server.handle(h1); - Awaitility.await() - .atMost(Duration.ofSeconds(30)) - .until(h1.getServletResponse()::getStatus, (status) -> status == 204); - MockHttpConnection h2 = new MockHttpConnection(); - this.server.handle(h2); - Awaitility.await() - .atMost(Duration.ofSeconds(30)) - .until(h2.getServletResponse()::getStatus, (status) -> status == 204); - this.serverChannel.disconnect(); - this.server.getServerThread().join(JOIN_TIMEOUT); - } - - @Test - void disconnectTimeout() throws Exception { - givenServerConnectionOpenWillAnswerWithServerChannel(); - this.server.setDisconnectTimeout(100); - this.server.setLongPollTimeout(100); - MockHttpConnection h1 = new MockHttpConnection(); - this.server.handle(h1); - this.serverChannel.send("hello"); - this.server.getServerThread().join(JOIN_TIMEOUT); - assertThat(this.serverChannel.isOpen()).isFalse(); - } - - @Test - void disconnectTimeoutMustBePositive() { - assertThatIllegalArgumentException().isThrownBy(() -> this.server.setDisconnectTimeout(0)) - .withMessageContaining("DisconnectTimeout must be a positive value"); - } - - @Test - void httpConnectionRespondWithPayload() throws Exception { - HttpConnection connection = new HttpConnection(this.request, this.response); - connection.waitForResponse(); - connection.respond(new HttpTunnelPayload(1, ByteBuffer.wrap("hello".getBytes()))); - assertThat(this.servletResponse.getStatus()).isEqualTo(200); - assertThat(this.servletResponse.getContentAsString()).isEqualTo("hello"); - assertThat(this.servletResponse.getHeader(SEQ_HEADER)).isEqualTo("1"); - } - - @Test - void httpConnectionRespondWithStatus() throws Exception { - HttpConnection connection = new HttpConnection(this.request, this.response); - connection.waitForResponse(); - connection.respond(HttpStatus.I_AM_A_TEAPOT); - assertThat(this.servletResponse.getStatus()).isEqualTo(418); - assertThat(this.servletResponse.getContentLength()).isEqualTo(0); - } - - @Test - void httpConnectionAsync() throws Exception { - ServerHttpAsyncRequestControl async = mock(ServerHttpAsyncRequestControl.class); - ServerHttpRequest request = mock(ServerHttpRequest.class); - given(request.getAsyncRequestControl(this.response)).willReturn(async); - HttpConnection connection = new HttpConnection(request, this.response); - connection.waitForResponse(); - then(async).should().start(); - connection.respond(HttpStatus.NO_CONTENT); - then(async).should().complete(); - } - - @Test - void httpConnectionNonAsync() throws Exception { - testHttpConnectionNonAsync(0); - testHttpConnectionNonAsync(100); - } - - private void testHttpConnectionNonAsync(long sleepBeforeResponse) throws IOException, InterruptedException { - ServerHttpRequest request = mock(ServerHttpRequest.class); - given(request.getAsyncRequestControl(this.response)).willThrow(new IllegalArgumentException()); - final HttpConnection connection = new HttpConnection(request, this.response); - final AtomicBoolean responded = new AtomicBoolean(); - Thread connectionThread = new Thread(() -> { - connection.waitForResponse(); - responded.set(true); - }); - connectionThread.start(); - assertThat(responded.get()).isFalse(); - Thread.sleep(sleepBeforeResponse); - connection.respond(HttpStatus.NO_CONTENT); - connectionThread.join(); - assertThat(responded.get()).isTrue(); - } - - @Test - void httpConnectionRunning() throws Exception { - HttpConnection connection = new HttpConnection(this.request, this.response); - assertThat(connection.isOlderThan(100)).isFalse(); - Thread.sleep(200); - assertThat(connection.isOlderThan(100)).isTrue(); - } - - private void givenServerConnectionOpenWillAnswerWithServerChannel() throws IOException { - given(this.serverConnection.open(anyInt())).willAnswer((invocation) -> { - MockServerChannel channel = HttpTunnelServerTests.this.serverChannel; - channel.setTimeout(invocation.getArgument(0)); - return channel; - }); - } - - /** - * Mock {@link ByteChannel} used to simulate the server connection. - */ - static class MockServerChannel implements ByteChannel { - - private static final ByteBuffer DISCONNECT = ByteBuffer.wrap(NO_DATA); - - private int timeout; - - private BlockingDeque outgoing = new LinkedBlockingDeque<>(); - - private ByteArrayOutputStream written = new ByteArrayOutputStream(); - - private AtomicBoolean open = new AtomicBoolean(true); - - void setTimeout(int timeout) { - this.timeout = timeout; - } - - void send(String content) { - send(content.getBytes()); - } - - void send(byte[] bytes) { - this.outgoing.addLast(ByteBuffer.wrap(bytes)); - } - - void disconnect() { - this.outgoing.addLast(DISCONNECT); - } - - void verifyReceived(String expected) { - verifyReceived(expected.getBytes()); - } - - void verifyReceived(byte[] expected) { - synchronized (this.written) { - assertThat(this.written.toByteArray()).isEqualTo(expected); - this.written.reset(); - } - } - - @Override - public int read(ByteBuffer dst) throws IOException { - try { - ByteBuffer bytes = this.outgoing.pollFirst(this.timeout, TimeUnit.MILLISECONDS); - if (bytes == null) { - throw new SocketTimeoutException(); - } - if (bytes == DISCONNECT) { - this.open.set(false); - return -1; - } - int initialRemaining = dst.remaining(); - bytes.limit(Math.min(bytes.limit(), initialRemaining)); - dst.put(bytes); - bytes.limit(bytes.capacity()); - return initialRemaining - dst.remaining(); - } - catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - } - - @Override - public int write(ByteBuffer src) throws IOException { - int remaining = src.remaining(); - synchronized (this.written) { - Channels.newChannel(this.written).write(src); - } - return remaining; - } - - @Override - public boolean isOpen() { - return this.open.get(); - } - - @Override - public void close() { - this.open.set(false); - } - - } - - /** - * Mock {@link HttpConnection}. - */ - static class MockHttpConnection extends HttpConnection { - - MockHttpConnection() { - super(new ServletServerHttpRequest(new MockHttpServletRequest()), - new ServletServerHttpResponse(new MockHttpServletResponse())); - } - - MockHttpConnection(String content, int seq) { - this(); - MockHttpServletRequest request = getServletRequest(); - request.setContent(content.getBytes()); - request.addHeader(SEQ_HEADER, String.valueOf(seq)); - } - - @Override - protected ServerHttpAsyncRequestControl startAsync() { - getServletRequest().setAsyncSupported(true); - return super.startAsync(); - } - - @Override - protected void complete() { - super.complete(); - getServletResponse().setCommitted(true); - } - - MockHttpServletRequest getServletRequest() { - return (MockHttpServletRequest) ((ServletServerHttpRequest) getRequest()).getServletRequest(); - } - - MockHttpServletResponse getServletResponse() { - return (MockHttpServletResponse) ((ServletServerHttpResponse) getResponse()).getServletResponse(); - } - - void verifyReceived(String expectedContent, int expectedSeq) throws Exception { - waitForServletResponse(); - MockHttpServletResponse resp = getServletResponse(); - assertThat(resp.getContentAsString()).isEqualTo(expectedContent); - assertThat(resp.getHeader(SEQ_HEADER)).isEqualTo(String.valueOf(expectedSeq)); - } - - void waitForServletResponse() throws InterruptedException { - while (!getServletResponse().isCommitted()) { - Thread.sleep(10); - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnectionTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnectionTests.java deleted file mode 100644 index d6a3e951db..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/SocketTargetServerConnectionTests.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link SocketTargetServerConnection}. - * - * @author Phillip Webb - */ -class SocketTargetServerConnectionTests { - - private static final int DEFAULT_TIMEOUT = 5000; - - private MockServer server; - - private SocketTargetServerConnection connection; - - @BeforeEach - void setup() throws IOException { - this.server = new MockServer(); - this.connection = new SocketTargetServerConnection(() -> this.server.getPort()); - } - - @Test - void readData() throws Exception { - this.server.willSend("hello".getBytes()); - this.server.start(); - ByteChannel channel = this.connection.open(DEFAULT_TIMEOUT); - ByteBuffer buffer = ByteBuffer.allocate(5); - channel.read(buffer); - assertThat(buffer.array()).isEqualTo("hello".getBytes()); - } - - @Test - void writeData() throws Exception { - this.server.expect("hello".getBytes()); - this.server.start(); - ByteChannel channel = this.connection.open(DEFAULT_TIMEOUT); - ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); - channel.write(buffer); - this.server.closeAndVerify(); - } - - @Test - void timeout() throws Exception { - this.server.delay(1000); - this.server.start(); - ByteChannel channel = this.connection.open(10); - long startTime = System.currentTimeMillis(); - assertThatExceptionOfType(SocketTimeoutException.class).isThrownBy(() -> channel.read(ByteBuffer.allocate(5))) - .satisfies((ex) -> { - long runTime = System.currentTimeMillis() - startTime; - assertThat(runTime).isGreaterThanOrEqualTo(10L); - assertThat(runTime).isLessThan(10000L); - }); - } - - static class MockServer { - - private ServerSocketChannel serverSocket; - - private byte[] send; - - private byte[] expect; - - private int delay; - - private ByteBuffer actualRead; - - private ServerThread thread; - - MockServer() throws IOException { - this.serverSocket = ServerSocketChannel.open(); - this.serverSocket.bind(new InetSocketAddress(0)); - } - - int getPort() { - return this.serverSocket.socket().getLocalPort(); - } - - void delay(int delay) { - this.delay = delay; - } - - void willSend(byte[] send) { - this.send = send; - } - - void expect(byte[] expect) { - this.expect = expect; - } - - void start() { - this.thread = new ServerThread(); - this.thread.start(); - } - - void closeAndVerify() throws InterruptedException { - close(); - assertThat(this.actualRead.array()).isEqualTo(this.expect); - } - - void close() throws InterruptedException { - while (this.thread.isAlive()) { - Thread.sleep(10); - } - } - - private class ServerThread extends Thread { - - @Override - public void run() { - try { - SocketChannel channel = MockServer.this.serverSocket.accept(); - Thread.sleep(MockServer.this.delay); - if (MockServer.this.send != null) { - ByteBuffer buffer = ByteBuffer.wrap(MockServer.this.send); - while (buffer.hasRemaining()) { - channel.write(buffer); - } - } - if (MockServer.this.expect != null) { - ByteBuffer buffer = ByteBuffer.allocate(MockServer.this.expect.length); - while (buffer.hasRemaining()) { - channel.read(buffer); - } - MockServer.this.actualRead = buffer; - } - channel.close(); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/StaticPortProviderTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/StaticPortProviderTests.java deleted file mode 100644 index 961074e8e3..0000000000 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/server/StaticPortProviderTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.devtools.tunnel.server; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link StaticPortProvider}. - * - * @author Phillip Webb - */ -class StaticPortProviderTests { - - @Test - void portMustBePositive() { - assertThatIllegalArgumentException().isThrownBy(() -> new StaticPortProvider(0)) - .withMessageContaining("Port must be positive"); - } - - @Test - void getPort() { - StaticPortProvider provider = new StaticPortProvider(123); - assertThat(provider.getPort()).isEqualTo(123); - } - -}