Jetty 12 - Cleanup Shutdown classes (#9201)

* Fixed ShutdownHandler in jetty-core
* Delete ee9 ShutdownHandler
* Rename GracefulShutdownHandler to just GracefulHandler
* Adding graceful Jetty module
* Improved Javadoc + Token encoding tests
This commit is contained in:
Joakim Erdfelt 2023-01-25 13:54:53 -06:00 committed by GitHub
parent e271629cfc
commit 81f7031cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 323 additions and 255 deletions

View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<!-- =============================================================== -->
<!-- Mixin the Graceful Handler -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="insertHandler">
<Arg>
<New id="GracefulHandler" class="org.eclipse.jetty.server.handler.GracefulHandler" />
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,18 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Enables Graceful processing of requests
[tags]
server
[depend]
server
[xml]
etc/jetty-graceful.xml
[ini-template]
## If the Graceful shutdown should wait for async requests as well as the currently dispatched ones.
# jetty.statistics.gracefulShutdownWaitsForRequests=true

View File

@ -28,14 +28,14 @@ import org.slf4j.LoggerFactory;
/**
* Handler to track active requests and allow them to gracefully complete.
*/
public class GracefulShutdownHandler extends Handler.Wrapper implements Graceful
public class GracefulHandler extends Handler.Wrapper implements Graceful
{
private static final Logger LOG = LoggerFactory.getLogger(GracefulShutdownHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(GracefulHandler.class);
private final LongAdder dispatchedStats = new LongAdder();
private final Shutdown shutdown;
public GracefulShutdownHandler()
public GracefulHandler()
{
shutdown = new Shutdown(this)
{

View File

@ -13,230 +13,200 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketException;
import java.net.URL;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A handler that shuts the server down on a valid request. Used to do "soft" restarts from Java.
* If _exitJvm is set to true a hard System.exit() call is being made.
* If _sendShutdownAtStart is set to true, starting the server will try to shut down an existing server at the same port.
* If _sendShutdownAtStart is set to true, make an http call to
* "http://localhost:" + port + "/shutdown?token=" + shutdownCookie
* in order to shut down the server.
* <p>
* A {@link Handler} that initiates a Shutdown of the Jetty Server it belongs to.
* </p>
*
* This handler is a contribution from Johannes Brodwall: https://bugs.eclipse.org/bugs/show_bug.cgi?id=357687
* <p>
* Used to trigger shutdown of a Jetty Server instance
* <ul>
* <li>If {@code exitJvm} is set to true a hard {@link System#exit(int)} call will be performed.</li>
* </ul>
*
* Usage:
* Server Setup Example:
*
* <pre>
* <pre>{@code
* Server server = new Server(8080);
* ShutdownHandler shutdown = new ShutdownHandler(&quot;secret password&quot;, false, true) });
* server.setHandler(shutdown);
* String shutdownToken = "secret password";
* boolean exitJvm = false;
* ShutdownHandler shutdown = new ShutdownHandler(shutdownToken, exitJvm));
* shutdown.setHandler(someOtherHandler);
* server.setHandler(someOtherHandlers);
* server.start();
* </pre>
* }</pre>
*
* <pre>
* public static void attemptShutdown(int port, String shutdownCookie) {
* Client Triggering Example
*
* <pre>{@code
* public static void attemptShutdown(int port, String shutdownToken) {
* try {
* URL url = new URL("http://localhost:" + port + "/shutdown?token=" + shutdownCookie);
* HttpURLConnection connection = (HttpURLConnection)url.openConnection();
* connection.setRequestMethod("POST");
* connection.getResponseCode();
* logger.info("Shutting down " + url + ": " + connection.getResponseMessage());
* } catch (SocketException e) {
* logger.debug("Not running");
* String encodedToken = URLEncoder.encode(shutdownToken);
* URI uri = URI.create("http://localhost:%d/shutdown?token=%s".formatted(port, shutdownCookie));
* HttpClient httpClient = HttpClient.newBuilder().build();
* HttpRequest httpRequest = HttpRequest.newBuilder(shutdownURI)
* .POST(HttpRequest.BodyPublishers.noBody())
* .build();
* HttpResponse<String> httpResponse = httpClient.send(httpRequest,
* HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
* Assertions.assertEquals(200, httpResponse.statusCode());
* System.out.println(httpResponse.body());
* logger.info("Shutting down " + uri + ": " + httpResponse.body());
* } catch (IOException | InterruptedException e) {
* logger.debug("Shutdown Handler not available");
* // Okay - the server is not running
* } catch (IOException e) {
* throw new RuntimeException(e);
* }
* }
* </pre>
* }</pre>
*/
public class ShutdownHandler extends Handler.Wrapper
{
private static final Logger LOG = LoggerFactory.getLogger(ShutdownHandler.class);
private final String _shutdownPath;
private final String _shutdownToken;
private boolean _sendShutdownAtStart;
private boolean _exitJvm = false;
/**
* Creates a listener that lets the server be shut down remotely (but only from localhost).
* Creates a Handler that lets the server be shut down remotely (but only from localhost).
*
* @param shutdownToken a secret password to avoid unauthorized shutdown attempts
*/
public ShutdownHandler(String shutdownToken)
{
this(shutdownToken, false, false);
this(null, shutdownToken, false);
}
/**
* Creates a Handler that lets the server be shut down remotely (but only from localhost).
*
* @param shutdownToken a secret password to avoid unauthorized shutdown attempts
* @param exitJVM If true, when the shutdown is executed, the handler class System.exit()
* @param sendShutdownAtStart If true, a shutdown is sent as an HTTP post
* during startup, which will shutdown any previously running instances of
* this server with an identically configured ShutdownHandler
*/
public ShutdownHandler(String shutdownToken, boolean exitJVM, boolean sendShutdownAtStart)
public ShutdownHandler(String shutdownToken, boolean exitJVM)
{
this(null, shutdownToken, exitJVM);
}
/**
* Creates a Handler that lets the server be shut down remotely (but only from localhost).
*
* @param shutdownPath the path to respond to shutdown requests against (default is "{@code /shutdown}")
* @param shutdownToken a secret password to avoid unauthorized shutdown attempts
* @param exitJVM If true, when the shutdown is executed, the handler class System.exit()
*/
public ShutdownHandler(String shutdownPath, String shutdownToken, boolean exitJVM)
{
this._shutdownPath = StringUtil.isBlank(shutdownPath) ? "/shutdown" : shutdownPath;
this._shutdownToken = shutdownToken;
/* TODO
setExitJvm(exitJVM);
setSendShutdownAtStart(sendShutdownAtStart);
*/
}
public void sendShutdown() throws IOException
{
URL url = new URL(getServerUrl() + "/shutdown?token=" + _shutdownToken);
try
{
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("POST");
connection.getResponseCode();
LOG.info("Shutting down {}: {} {}", url, connection.getResponseCode(), connection.getResponseMessage());
}
catch (SocketException e)
{
LOG.debug("Not running");
// Okay - the server is not running
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@SuppressWarnings("resource")
private String getServerUrl()
{
NetworkConnector connector = null;
for (Connector c : getServer().getConnectors())
{
if (c instanceof NetworkConnector)
{
connector = (NetworkConnector)c;
break;
}
}
if (connector == null)
return "http://localhost";
return "http://localhost:" + connector.getPort();
}
@Override
protected void doStart() throws Exception
{
super.doStart();
if (_sendShutdownAtStart)
sendShutdown();
this._exitJvm = exitJVM;
}
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
{
return super.process(request, response, callback);
/* TODO
if (!target.equals("/shutdown"))
String fullPath = request.getHttpURI().getCanonicalPath();
ContextHandler contextHandler = ContextHandler.getContextHandler(request);
if (contextHandler != null)
{
super.handle(target, baseRequest, request, response);
return;
// We are operating in a context, so use it
String pathInContext = contextHandler.getContext().getPathInContext(fullPath);
if (!pathInContext.startsWith(this._shutdownPath))
{
return super.process(request, response, callback);
}
}
else
{
// We are standalone
if (!fullPath.startsWith(this._shutdownPath))
{
return super.process(request, response, callback);
}
}
if (!request.getMethod().equals("POST"))
{
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400);
return true;
}
if (!hasCorrectSecurityToken(request))
{
LOG.warn("Unauthorized tokenless shutdown attempt from {}", request.getRemoteAddr());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
LOG.warn("Unauthorized tokenless shutdown attempt from {}", request.getConnectionMetaData().getRemoteSocketAddress());
Response.writeError(request, response, callback, HttpStatus.UNAUTHORIZED_401);
return true;
}
if (!requestFromLocalhost(baseRequest))
if (!requestFromLocalhost(request))
{
LOG.warn("Unauthorized non-loopback shutdown attempt from {}", request.getRemoteAddr());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
LOG.warn("Unauthorized non-loopback shutdown attempt from {}", request.getConnectionMetaData().getRemoteSocketAddress());
Response.writeError(request, response, callback, HttpStatus.UNAUTHORIZED_401);
return true;
}
LOG.info("Shutting down by request from {}", request.getRemoteAddr());
doShutdown(baseRequest, response);
*/
}
/* TODO
protected void doShutdown(Request baseRequest, HttpServletResponse response) throws IOException
LOG.info("Shutting down by request from {}", request.getConnectionMetaData().getRemoteSocketAddress());
// Establish callback to trigger server shutdown when write of response is complete
Callback triggerShutdownCallback = Callback.from(() ->
{
for (Connector connector : getServer().getConnectors())
{
connector.shutdown();
}
baseRequest.setHandled(true);
response.setStatus(200);
response.flushBuffer();
final Server server = getServer();
new Thread()
{
@Override
public void run()
{
try
{
shutdownServer(server);
}
catch (InterruptedException e)
{
LOG.trace("IGNORED", e);
}
catch (Exception e)
{
throw new RuntimeException("Shutting down server", e);
}
}
}.start();
CompletableFuture.runAsync(this::shutdownServer);
});
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain, charset=utf-8");
String message = "Shutdown triggered";
Content.Sink.write(response, true, message, triggerShutdownCallback);
return true;
}
private boolean requestFromLocalhost(Request request)
{
InetSocketAddress addr = request.getRemoteInetSocketAddress();
if (addr == null)
{
SocketAddress socketAddress = request.getConnectionMetaData().getRemoteSocketAddress();
if (socketAddress == null)
return false;
if (socketAddress instanceof InetSocketAddress addr)
return addr.getAddress().isLoopbackAddress();
return false;
}
return addr.getAddress().isLoopbackAddress();
}
private boolean hasCorrectSecurityToken(HttpServletRequest request)
private boolean hasCorrectSecurityToken(Request request)
{
String tok = request.getParameter("token");
Fields fields = Request.extractQueryParameters(request);
String tok = fields.getValue("token");
if (LOG.isDebugEnabled())
LOG.debug("Token: {}", tok);
return _shutdownToken.equals(tok);
}
private void shutdownServer(Server server) throws Exception
private void shutdownServer()
{
server.stop();
try
{
// Let server stop normally.
// Order of stop is controlled by server.
// Graceful stop can even be configured at the Server level
getServer().stop();
}
catch (Exception e)
{
LOG.warn("Unable to stop server", e);
}
if (_exitJvm)
{
@ -249,25 +219,8 @@ public class ShutdownHandler extends Handler.Wrapper
this._exitJvm = exitJvm;
}
public boolean isSendShutdownAtStart()
{
return _sendShutdownAtStart;
}
public void setSendShutdownAtStart(boolean sendShutdownAtStart)
{
_sendShutdownAtStart = sendShutdownAtStart;
}
public String getShutdownToken()
{
return _shutdownToken;
}
public boolean isExitJvm()
{
return _exitJvm;
}
*/
}

View File

@ -29,7 +29,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.handler.GracefulShutdownHandler;
import org.eclipse.jetty.server.handler.GracefulHandler;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.NanoTime;
@ -48,9 +48,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class GracefulShutdownTest
public class GracefulHandlerTest
{
private static final Logger LOG = LoggerFactory.getLogger(GracefulShutdownTest.class);
private static final Logger LOG = LoggerFactory.getLogger(GracefulHandlerTest.class);
private Server server;
private ServerConnector connector;
@ -113,13 +113,13 @@ public class GracefulShutdownTest
/**
* Test for when a Handler throws an unhandled Exception from {@link Handler#process(Request, Response, Callback)}
* when in normal mode (not during graceful mode). This test exists to ensure that the Callback management of
* the {@link GracefulShutdownHandler} doesn't mess with normal operations of requests.
* the {@link GracefulHandler} doesn't mess with normal operations of requests.
*/
@Test
public void testHandlerNormalUnhandledException() throws Exception
{
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
@ -127,7 +127,7 @@ public class GracefulShutdownTest
throw new RuntimeException("Intentional Exception");
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -175,8 +175,8 @@ public class GracefulShutdownTest
public void testHandlerGracefulUnhandledException() throws Exception
{
CountDownLatch dispatchLatch = new CountDownLatch(1);
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
@ -185,11 +185,11 @@ public class GracefulShutdownTest
// let main thread know that we've reach this handler
dispatchLatch.countDown();
// now wait for graceful stop to begin
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
throw new RuntimeException("Intentional Failure");
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -234,14 +234,14 @@ public class GracefulShutdownTest
/**
* Test for when a Handler uses {@link Callback#failed(Throwable)} when in normal mode (not during graceful mode).
* This test exists to ensure that the Callback management of the {@link GracefulShutdownHandler} doesn't
* This test exists to ensure that the Callback management of the {@link GracefulHandler} doesn't
* mess with normal operations of requests.
*/
@Test
public void testHandlerNormalCallbackFailure() throws Exception
{
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
@ -250,7 +250,7 @@ public class GracefulShutdownTest
return true;
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -298,20 +298,20 @@ public class GracefulShutdownTest
public void testHandlerGracefulCallbackFailure() throws Exception
{
CountDownLatch dispatchLatch = new CountDownLatch(1);
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
{
dispatchLatch.countDown();
// wait for graceful to kick in
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
callback.failed(new RuntimeException("Intentional Failure"));
return true;
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -357,14 +357,14 @@ public class GracefulShutdownTest
/**
* Test for when a Handler returns false from {@link Handler#process(Request, Response, Callback)}
* when in normal mode (not during graceful mode).
* This test exists to ensure that the Callback management of the {@link GracefulShutdownHandler} doesn't
* This test exists to ensure that the Callback management of the {@link GracefulHandler} doesn't
* mess with normal operations of requests.
*/
@Test
public void testHandlerNormalProcessingFalse() throws Exception
{
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
@ -372,7 +372,7 @@ public class GracefulShutdownTest
return false;
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -416,18 +416,18 @@ public class GracefulShutdownTest
public void testHandlerGracefulProcessingFalse() throws Exception
{
AtomicReference<CompletableFuture<Long>> stopFuture = new AtomicReference<>();
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new Handler.Abstract()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
{
stopFuture.set(runAsyncServerStop());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
return false;
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -471,8 +471,8 @@ public class GracefulShutdownTest
public void testHandlerGracefulBlocked() throws Exception
{
CountDownLatch dispatchedToHandlerLatch = new CountDownLatch(1);
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new BlockingReadHandler()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new BlockingReadHandler()
{
@Override
protected void onBeforeRead(Request request, Response response)
@ -480,7 +480,7 @@ public class GracefulShutdownTest
dispatchedToHandlerLatch.countDown();
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -508,7 +508,7 @@ public class GracefulShutdownTest
CompletableFuture<Long> stopFuture = runAsyncServerStop();
// Wait till we enter graceful mode
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
// Send rest of data
output0.write("67890".getBytes(StandardCharsets.UTF_8));
@ -544,8 +544,8 @@ public class GracefulShutdownTest
public void testHandlerGracefulBlockedEarlyCommit() throws Exception
{
CountDownLatch dispatchedToHandlerLatch = new CountDownLatch(1);
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new BlockingReadHandler()
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new BlockingReadHandler()
{
@Override
protected void onBeforeRead(Request request, Response response) throws Exception
@ -560,7 +560,7 @@ public class GracefulShutdownTest
dispatchedToHandlerLatch.countDown();
}
});
server = createServer(gracefulShutdownHandler);
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -588,7 +588,7 @@ public class GracefulShutdownTest
CompletableFuture<Long> stopFuture = runAsyncServerStop();
// Wait till we enter graceful mode
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
// Send rest of data
output0.write("67890".getBytes(StandardCharsets.UTF_8));
@ -614,15 +614,15 @@ public class GracefulShutdownTest
}
/**
* Test of how the {@link GracefulShutdownHandler} should behave if it
* Test of how the {@link GracefulHandler} should behave if it
* receives a request on an active connection after graceful starts.
*/
@Test
public void testRequestAfterGraceful() throws Exception
{
GracefulShutdownHandler gracefulShutdownHandler = new GracefulShutdownHandler();
gracefulShutdownHandler.setHandler(new BlockingReadHandler());
server = createServer(gracefulShutdownHandler);
GracefulHandler gracefulHandler = new GracefulHandler();
gracefulHandler.setHandler(new BlockingReadHandler());
server = createServer(gracefulHandler);
server.setStopTimeout(10000);
server.start();
@ -655,7 +655,7 @@ public class GracefulShutdownTest
CompletableFuture<Long> stopFuture = runAsyncServerStop();
// Wait till we enter graceful mode
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulShutdownHandler.isShutdown());
await().atMost(5, TimeUnit.SECONDS).until(() -> gracefulHandler.isShutdown());
// Send another request on same connection
output0.write(rawRequest.formatted(2).getBytes(StandardCharsets.UTF_8));

View File

@ -13,53 +13,69 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled // TODO
public class ShutdownHandlerTest
{
private Server server;
private ServerConnector connector;
private String shutdownToken = "asdlnsldgnklns";
public void start(Handler.Wrapper wrapper) throws Exception
public void createServer(Handler handler) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
Handler shutdown = new ShutdownHandler(shutdownToken);
Handler handler = shutdown;
if (wrapper != null)
{
wrapper.setHandler(shutdown);
handler = wrapper;
}
server.setHandler(handler);
server.start();
}
@Test
public void testShutdownServerWithCorrectTokenAndIP() throws Exception
@AfterEach
public void teardown()
{
start(null);
LifeCycle.stop(server);
}
@ParameterizedTest
@ValueSource(strings = {"abcdefg", "a token with space", "euro-€-token"})
public void testShutdownServerWithCorrectTokenAndFromLocalhost(String shutdownToken) throws Exception
{
ShutdownHandler shutdownHandler = new ShutdownHandler(shutdownToken);
shutdownHandler.setHandler(new EchoHandler());
InetSocketAddress fakeRemoteAddr = new InetSocketAddress("127.0.0.1", 22033);
Handler.Wrapper fakeRemoteAddressHandler = new FakeRemoteAddressHandlerWrapper(fakeRemoteAddr);
fakeRemoteAddressHandler.setHandler(shutdownHandler);
createServer(fakeRemoteAddressHandler);
server.start();
CountDownLatch stopLatch = new CountDownLatch(1);
server.addEventListener(new AbstractLifeCycle.AbstractLifeCycleListener()
@ -71,7 +87,7 @@ public class ShutdownHandlerTest
}
});
HttpTester.Response response = shutdown(shutdownToken);
HttpTester.Response response = sendShutdownRequest(shutdownToken);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(stopLatch.await(5, TimeUnit.SECONDS));
@ -81,9 +97,13 @@ public class ShutdownHandlerTest
@Test
public void testWrongToken() throws Exception
{
start(null);
String shutdownToken = "abcdefg";
ShutdownHandler shutdownHandler = new ShutdownHandler(shutdownToken);
shutdownHandler.setHandler(new EchoHandler());
createServer(shutdownHandler);
server.start();
HttpTester.Response response = shutdown("wrongToken");
HttpTester.Response response = sendShutdownRequest("wrongToken");
assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
Thread.sleep(1000);
@ -93,40 +113,103 @@ public class ShutdownHandlerTest
@Test
public void testShutdownRequestNotFromLocalhost() throws Exception
{
/* TODO
start(new Handler.Wrapper()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setRemoteAddr(new InetSocketAddress("192.168.0.1", 12345));
super.handle(target, baseRequest, request, response);
}
});
String shutdownToken = "abcdefg";
*/
ShutdownHandler shutdownHandler = new ShutdownHandler(shutdownToken);
shutdownHandler.setHandler(new EchoHandler());
HttpTester.Response response = shutdown(shutdownToken);
InetSocketAddress fakeRemoteAddr = new InetSocketAddress("192.168.0.1", 12345);
Handler.Wrapper fakeRemoteAddressHandler = new FakeRemoteAddressHandlerWrapper(fakeRemoteAddr);
fakeRemoteAddressHandler.setHandler(shutdownHandler);
createServer(fakeRemoteAddressHandler);
server.start();
HttpTester.Response response = sendShutdownRequest(shutdownToken);
assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
Thread.sleep(1000);
assertEquals(AbstractLifeCycle.STARTED, server.getState());
}
private HttpTester.Response shutdown(String shutdownToken) throws IOException
private HttpTester.Response sendShutdownRequest(String shutdownToken) throws Exception
{
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
URI shutdownUri = server.getURI().resolve("/shutdown?token=" + URLEncoder.encode(shutdownToken, StandardCharsets.UTF_8));
try (Socket client = new Socket(shutdownUri.getHost(), shutdownUri.getPort());
OutputStream output = client.getOutputStream();
InputStream input = client.getInputStream())
{
String request =
"POST /shutdown?token=" + shutdownToken + " HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n";
OutputStream output = socket.getOutputStream();
output.write(request.getBytes(StandardCharsets.UTF_8));
String rawRequest = """
POST %s?%s HTTP/1.1
Host: %s:%d
Connection: close
Content-Length: 0
""".formatted(shutdownUri.getRawPath(), shutdownUri.getRawQuery(), shutdownUri.getHost(), shutdownUri.getPort());
output.write(rawRequest.getBytes(StandardCharsets.UTF_8));
output.flush();
HttpTester.Input input = HttpTester.from(socket.getInputStream());
return HttpTester.parseResponse(input);
HttpTester.Response response = HttpTester.parseResponse(input);
return response;
}
}
static class FakeRemoteAddressHandlerWrapper extends Handler.Wrapper
{
private final InetSocketAddress fakeRemoteAddress;
public FakeRemoteAddressHandlerWrapper(InetSocketAddress fakeRemoteAddress)
{
super();
this.fakeRemoteAddress = fakeRemoteAddress;
}
@Override
public boolean process(Request request, Response response, Callback callback) throws Exception
{
Request fakedRequest = FakeRemoteAddressRequest.from(request, this.fakeRemoteAddress);
return super.process(fakedRequest, response, callback);
}
}
static class FakeRemoteAddressConnectionMetadata extends ConnectionMetaData.Wrapper
{
private final InetSocketAddress fakeRemoteAddress;
public FakeRemoteAddressConnectionMetadata(ConnectionMetaData wrapped, InetSocketAddress fakeRemoteAddress)
{
super(wrapped);
this.fakeRemoteAddress = fakeRemoteAddress;
}
@Override
public SocketAddress getRemoteSocketAddress()
{
return this.fakeRemoteAddress;
}
}
static class FakeRemoteAddressRequest extends Request.Wrapper
{
private final ConnectionMetaData fakeConnectionMetaData;
public static Request from(Request request, InetSocketAddress fakeRemoteAddress)
{
ConnectionMetaData fakeRemoteConnectionMetadata = new FakeRemoteAddressConnectionMetadata(request.getConnectionMetaData(), fakeRemoteAddress);
return new FakeRemoteAddressRequest(request, fakeRemoteConnectionMetadata);
}
public FakeRemoteAddressRequest(Request wrapped, ConnectionMetaData fakeRemoteConnectionMetadata)
{
super(wrapped);
this.fakeConnectionMetaData = fakeRemoteConnectionMetadata;
}
@Override
public ConnectionMetaData getConnectionMetaData()
{
return this.fakeConnectionMetaData;
}
}
}