Cleanup remote debug tunnel leftovers from devtools
Closes gh-36808
This commit is contained in:
@@ -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<String> 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<String> 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<HandlerMapper> 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ByteBuffer> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user