diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index d1ad6b93ca3..84fbc3cd238 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.http.MultiPartCompliance; import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.internal.CompletionStreamWrapper; import org.eclipse.jetty.server.internal.HttpChannelState; import org.eclipse.jetty.util.Attributes; @@ -716,6 +717,7 @@ public interface Request extends Attributes, Content.Source * is returned, then this method must not generate a response, nor complete the callback. * @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be * called and thus should attempt to complete the request as if a false had been returned. + * @see AbortException */ boolean handle(Request request, Response response, Callback callback) throws Exception; @@ -725,6 +727,34 @@ public interface Request extends Attributes, Content.Source { return InvocationType.BLOCKING; } + + /** + * A marker {@link Exception} that can be passed the {@link Callback#failed(Throwable)} of the {@link Callback} + * passed in {@link #handle(Request, Response, Callback)}, to cause request handling to be aborted. For HTTP/1 + * an abort is handled with a {@link EndPoint#close()}, for later versions of HTTP, a reset message will be sent. + */ + class AbortException extends Exception + { + public AbortException() + { + super(); + } + + public AbortException(String message) + { + super(message); + } + + public AbortException(String message, Throwable cause) + { + super(message, cause); + } + + public AbortException(Throwable cause) + { + super(cause); + } + } } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index f3455d38095..d2068f6381e 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -1590,18 +1590,18 @@ public class HttpChannelState implements HttpChannel, Components httpChannelState._callbackFailure = failure; - // Consume any input. - Throwable unconsumed = stream.consumeAvailable(); - ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); + if (!stream.isCommitted() && !(failure instanceof Request.Handler.AbortException)) + { + // Consume any input. + Throwable unconsumed = stream.consumeAvailable(); + ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); - ChannelResponse response = httpChannelState._response; - if (LOG.isDebugEnabled()) - LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this); + ChannelResponse response = httpChannelState._response; + if (LOG.isDebugEnabled()) + LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this); - // There may have been an attempt to write an error response that failed. - // Do not try to write again an error response if already committed. - if (!stream.isCommitted()) errorResponse = new ErrorResponse(request); + } } if (errorResponse != null) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index 8b9510164d7..7c21e92731d 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -816,4 +816,25 @@ public class TypeUtil int result = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); return result > 0 ? result : Integer.MAX_VALUE; } + + /** + * Test is a method has been declared on the class of an instance + * @param object The object to check + * @param methodName The method name + * @param args The arguments to the method + * @return {@code true} iff {@link Class#getDeclaredMethod(String, Class[])} can be called on the + * {@link Class} of the object, without throwing {@link NoSuchMethodException}. + */ + public static boolean isDeclaredMethodOn(Object object, String methodName, Class... args) + { + try + { + object.getClass().getDeclaredMethod(methodName, args); + return true; + } + catch (NoSuchMethodException e) + { + return false; + } + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 83cf59cd1d8..ee5772c5d6c 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -77,6 +77,7 @@ import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -86,6 +87,7 @@ import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.Resources; import org.eclipse.jetty.util.security.CertificateUtils; import org.eclipse.jetty.util.security.CertificateValidator; +import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Password; import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; @@ -157,9 +159,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du private Resource _trustStoreResource; private String _trustStoreProvider; private String _trustStoreType; - private Password _keyStorePassword; - private Password _keyManagerPassword; - private Password _trustStorePassword; + private Credential _keyStoreCredential; + private Credential _keyManagerCredential; + private Credential _trustStoreCredential; private String _sslProvider; private String _sslProtocol = "TLS"; private String _secureRandomAlgorithm; @@ -811,46 +813,42 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du public String getKeyStorePassword() { - return _keyStorePassword == null ? null : _keyStorePassword.toString(); + return _keyStoreCredential == null ? null : _keyStoreCredential.toString(); } /** - * @param password The password for the key store. If null is passed and - * a keystore is set, then - * the {@link #getPassword(String)} is used to - * obtain a password either from the {@value #PASSWORD_PROPERTY} - * system property. + * @param password The password for the key store. If null is passed then + * {@link #getCredential(String)} is used to obtain a password from + * the {@value #PASSWORD_PROPERTY} system property. */ public void setKeyStorePassword(String password) { - _keyStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password); + _keyStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password); } public String getKeyManagerPassword() { - return _keyManagerPassword == null ? null : _keyManagerPassword.toString(); + return _keyManagerCredential == null ? null : _keyManagerCredential.toString(); } /** * @param password The password (if any) for the specific key within the key store. - * If null is passed and the {@value #KEYPASSWORD_PROPERTY} system property is set, - * then the {@link #getPassword(String)} is used to + * If null is passed then {@link #getCredential(String)} is used to * obtain a password from the {@value #KEYPASSWORD_PROPERTY} system property. */ public void setKeyManagerPassword(String password) { - _keyManagerPassword = password == null ? getPassword(KEYPASSWORD_PROPERTY) : newPassword(password); + _keyManagerCredential = password == null ? getCredential(KEYPASSWORD_PROPERTY) : newCredential(password); } /** * @param password The password for the truststore. If null is passed then - * the {@link #getPassword(String)} is used to - * obtain a password from the {@value #PASSWORD_PROPERTY} + * {@link #getCredential(String)} is used to obtain a password from the {@value #PASSWORD_PROPERTY} * system property. */ public void setTrustStorePassword(String password) { - _trustStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password); + _trustStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password); } /** @@ -1133,7 +1131,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du */ protected KeyStore loadKeyStore(Resource resource) throws Exception { - String storePassword = Objects.toString(_keyStorePassword, null); + String storePassword = Objects.toString(_keyStoreCredential, null); return CertificateUtils.getKeyStore(resource, getKeyStoreType(), getKeyStoreProvider(), storePassword); } @@ -1148,12 +1146,12 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du { String type = Objects.toString(getTrustStoreType(), getKeyStoreType()); String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider()); - Password passwd = _trustStorePassword; + Credential passwd = _trustStoreCredential; if (resource == null || resource.equals(_keyStoreResource)) { resource = _keyStoreResource; if (passwd == null) - passwd = _keyStorePassword; + passwd = _keyStoreCredential; } return CertificateUtils.getKeyStore(resource, type, provider, Objects.toString(passwd, null)); } @@ -1180,7 +1178,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du if (keyStore != null) { KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance(); - keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray()); + keyManagerFactory.init(keyStore, _keyManagerCredential == null ? (_keyStoreCredential == null ? null : _keyStoreCredential.toString().toCharArray()) : _keyManagerCredential.toString().toCharArray()); managers = keyManagerFactory.getKeyManagers(); if (managers != null) @@ -1615,7 +1613,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du * * @param realm the realm * @return the Password object + * @deprecated use {#link getCredential} instead. */ + @Deprecated(since = "12.0.13", forRemoval = true) protected Password getPassword(String realm) { String password = System.getProperty(realm); @@ -1627,12 +1627,43 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du * * @param password the password string * @return the new Password object + * @deprecated use {#link newCredential} instead. */ + @Deprecated(since = "12.0.13", forRemoval = true) public Password newPassword(String password) { return new Password(password); } + /** + * Returns the credential object for the given realm. + * + * @param realm the realm + * @return the Credential object + */ + protected Credential getCredential(String realm) + { + if (TypeUtil.isDeclaredMethodOn(this, "getPassword", String.class)) + return getPassword(realm); + + String password = System.getProperty(realm); + return password == null ? null : newCredential(password); + } + + /** + * Creates a new Credential object. + * + * @param password the password string + * @return the new Credential object + */ + public Credential newCredential(String password) + { + if (TypeUtil.isDeclaredMethodOn(this, "newPassword", String.class)) + return newPassword(password); + + return Credential.getCredential(password); + } + public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { checkIsStarted(); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java index 8432153365c..c57c079c937 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java @@ -267,4 +267,34 @@ public class TypeUtilTest assertThat(TypeUtil.ceilToNextPowerOfTwo(5), is(8)); assertThat(TypeUtil.ceilToNextPowerOfTwo(Integer.MAX_VALUE - 1), is(Integer.MAX_VALUE)); } + + public static class Base + { + protected String methodA(String arg) + { + return "a" + arg.length(); + } + + protected String methodB(String arg) + { + return "b" + arg.length(); + } + } + + public static class Example extends Base + { + @Override + protected String methodB(String arg) + { + return "B" + arg; + } + } + + @Test + public void testIsMethodDeclaredOn() + { + Example example = new Example(); + assertFalse(TypeUtil.isDeclaredMethodOn(example, "methodA", String.class)); + assertTrue(TypeUtil.isDeclaredMethodOn(example, "methodB", String.class)); + } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java index 6baaf8e7b99..c7e6d755be3 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java @@ -140,7 +140,7 @@ public class ServletApiResponse implements HttpServletResponse { switch (sc) { - case -1 -> getServletChannel().abort(new IOException(msg)); + case -1 -> getServletChannel().abort(new Request.Handler.AbortException(msg)); case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 -> { if (!isCommitted()) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java index c1b3d8234d8..3667b60f8dd 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java @@ -13,8 +13,12 @@ package org.eclipse.jetty.ee10.servlet; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -50,6 +54,7 @@ import org.eclipse.jetty.server.LocalConnector; 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.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -70,6 +75,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -780,6 +786,220 @@ public class ErrorPageTest assertThat(responseBody, Matchers.containsString("ERROR_REQUEST_URI: /fail/599")); } + @Test + public void testAbortWithSendError() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorChunked() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.getOutputStream().write("test".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertTrue(chunked); + + line = in.readLine(); + assertThat(line, is("4")); + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorContent() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(10); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorComplete() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(6); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + @Test public void testErrorCodeNoDefaultServletNonExistentErrorLocation() throws Exception { diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java index eee3c612f2d..971872c3b42 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java @@ -143,7 +143,7 @@ public class ServletApiResponse implements HttpServletResponse { switch (sc) { - case -1 -> getServletChannel().abort(new IOException(msg)); + case -1 -> getServletChannel().abort(new Request.Handler.AbortException(msg)); case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 -> { if (!isCommitted()) diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java index 85303aa1e84..36f80112d30 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java @@ -13,8 +13,12 @@ package org.eclipse.jetty.ee11.servlet; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -50,6 +54,7 @@ import org.eclipse.jetty.server.LocalConnector; 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.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -70,6 +75,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -836,6 +842,220 @@ public class ErrorPageTest assertThat(responseBody, Matchers.containsString("ERROR_REQUEST_URI: /fail/599")); } + @Test + public void testAbortWithSendError() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorChunked() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.getOutputStream().write("test".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertTrue(chunked); + + line = in.readLine(); + assertThat(line, is("4")); + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorContent() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(10); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorComplete() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(6); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + @Test public void testErrorCodeNoDefaultServletNonExistentErrorLocation() throws Exception { diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index ee95e158a63..78df43d272c 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -488,7 +488,7 @@ public class Response implements HttpServletResponse switch (code) { - case -1 -> _channel.abort(new IOException(message)); + case -1 -> _channel.abort(new org.eclipse.jetty.server.Request.Handler.AbortException(message)); case HttpStatus.PROCESSING_102 -> sendProcessing(); case HttpStatus.EARLY_HINTS_103 -> sendEarlyHint(); default -> _channel.getState().sendError(code, message); diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java index 02b5682aa55..bbed1f62278 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java @@ -13,8 +13,12 @@ package org.eclipse.jetty.ee9.servlet; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.concurrent.ConcurrentHashMap; @@ -29,7 +33,6 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; -import jakarta.servlet.RequestDispatcher; import jakarta.servlet.Servlet; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; @@ -49,10 +52,10 @@ import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +65,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; //@Disabled // TODO @@ -105,6 +109,7 @@ public class ErrorPageTest _context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*"); _context.addServlet(ErrorContentTypeCharsetWriterInitializedServlet.class, "/error-mime-charset-writer/*"); _context.addServlet(ExceptionServlet.class, "/exception-servlet"); + _context.addServlet(AbortServlet.class, "/abort"); HandlerWrapper noopHandler = new HandlerWrapper() { @@ -300,6 +305,34 @@ public class ErrorPageTest assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /fail/code")); } + @Test + public void testAbortWithSendError() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertNull(line); + } + } + @Test public void testErrorException() throws Exception { @@ -871,4 +904,13 @@ public class ErrorPageTest super(rootCause); } } + + public static class AbortServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(-1); + } + } } diff --git a/jetty-integrations/jetty-nosql/pom.xml b/jetty-integrations/jetty-nosql/pom.xml index 40920f3417a..7600beb8678 100644 --- a/jetty-integrations/jetty-nosql/pom.xml +++ b/jetty-integrations/jetty-nosql/pom.xml @@ -23,10 +23,21 @@ org.mongodb - mongo-java-driver + bson ${mongodb.version} compile + + org.mongodb + mongodb-driver-core + ${mongodb.version} + compile + + + org.mongodb + mongodb-driver-sync + compile + org.slf4j slf4j-api diff --git a/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod b/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod index ea6e8f7b1d9..7d8123f936e 100644 --- a/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod +++ b/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod @@ -14,11 +14,15 @@ sessions sessions/mongo/${connection-type} [files] -maven://org.mongodb/mongo-java-driver/${mongodb.version}|lib/nosql/mongo-java-driver-${mongodb.version}.jar +maven://org.mongodb/mongodb-driver-sync/${mongodb.version}|lib/nosql/mongodb-driver-sync-${mongodb.version}.jar +maven://org.mongodb/mongodb-driver-core/${mongodb.version}|lib/nosql/mongodb-driver-core-${mongodb.version}.jar +maven://org.mongodb/bson/${mongodb.version}|lib/nosql/bson-${mongodb.version}.jar [lib] lib/jetty-nosql-${jetty.version}.jar -lib/nosql/mongo-java-driver-${mongodb.version}.jar +lib/nosql/mongodb-driver-sync-${mongodb.version}.jar +lib/nosql/mongodb-driver-core-${mongodb.version}.jar +lib/nosql/bson-${mongodb.version}.jar [license] The java driver for the MongoDB document-based database system is hosted on GitHub and released under the Apache 2.0 license. diff --git a/jetty-integrations/jetty-nosql/src/main/java/module-info.java b/jetty-integrations/jetty-nosql/src/main/java/module-info.java index 2ef01b87a2b..0729e1c4a5d 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/module-info.java +++ b/jetty-integrations/jetty-nosql/src/main/java/module-info.java @@ -13,7 +13,9 @@ module org.eclipse.jetty.nosql { - requires transitive mongo.java.driver; + requires transitive org.mongodb.driver.core; + requires transitive org.mongodb.driver.sync.client; + requires transitive org.mongodb.bson; requires transitive org.eclipse.jetty.session; exports org.eclipse.jetty.nosql; diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java index 211812c75ba..8e92f1b5945 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java @@ -62,7 +62,7 @@ public abstract class NoSqlSessionDataStore extends ObjectStreamSessionDataStore public Set getAllAttributeNames() { - return new HashSet(_attributes.keySet()); + return new HashSet<>(_attributes.keySet()); } } diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java index 3395f9bb1f7..10bdba01c80 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java @@ -15,22 +15,26 @@ package org.eclipse.jetty.nosql.mongodb; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DBCollection; -import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.MongoException; -import com.mongodb.WriteConcern; -import com.mongodb.WriteResult; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.Projections; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.Binary; import org.eclipse.jetty.nosql.NoSqlSessionDataStore; import org.eclipse.jetty.session.SessionContext; import org.eclipse.jetty.session.SessionData; @@ -155,15 +159,15 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore /** * Access to MongoDB */ - private DBCollection _dbSessions; + private MongoCollection _dbSessions; - public void setDBCollection(DBCollection collection) + public void setDBCollection(MongoCollection collection) { _dbSessions = collection; } @ManagedAttribute(value = "DBCollection", readonly = true) - public DBCollection getDBCollection() + public MongoCollection getDBCollection() { return _dbSessions; } @@ -171,7 +175,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore @Override public SessionData doLoad(String id) throws Exception { - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id)); + Document sessionDocument = _dbSessions.find(Filters.eq(__ID, id)).first(); try { @@ -191,7 +195,8 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION)); Long lastSaved = (Long)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTSAVED)); String lastNode = (String)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTNODE)); - byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES)); + Binary binary = ((Binary)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES))); + byte[] attributes = binary == null ? null : binary.getData(); Long created = (Long)sessionDocument.get(__CREATED); Long accessed = (Long)sessionDocument.get(__ACCESSED); @@ -202,7 +207,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore NoSqlSessionData data = null; // get the session for the context - DBObject sessionSubDocumentForContext = (DBObject)MongoUtils.getNestedValue(sessionDocument, getContextField()); + Document sessionSubDocumentForContext = (Document)MongoUtils.getNestedValue(sessionDocument, getContextField()); if (LOG.isDebugEnabled()) LOG.debug("attrs {}", sessionSubDocumentForContext); @@ -239,9 +244,9 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore else { //attributes have special serialized format - try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes);) + try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes)) { - deserializeAttributes(data, bais); + deserializeAttributes(data, bais); } } } @@ -269,17 +274,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore * Check if the session exists and if it does remove the context * associated with this session */ - BasicDBObject mongoKey = new BasicDBObject(__ID, id); + Bson filterId = Filters.eq(__ID, id); - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id)); + Document sessionDocument = _dbSessions.find(filterId).first(); if (sessionDocument != null) { - DBObject c = (DBObject)MongoUtils.getNestedValue(sessionDocument, __CONTEXT); + Document c = (Document)MongoUtils.getNestedValue(sessionDocument, __CONTEXT); if (c == null) { //delete whole doc - _dbSessions.remove(mongoKey, WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return false; } @@ -287,14 +292,14 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore if (contexts.isEmpty()) { //delete whole doc - _dbSessions.remove(mongoKey, WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return false; } if (contexts.size() == 1 && contexts.iterator().next().equals(getCanonicalContextId())) { //delete whole doc - _dbSessions.remove(new BasicDBObject(__ID, id), WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return true; } @@ -303,7 +308,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore BasicDBObject unsets = new BasicDBObject(); unsets.put(getContextField(), 1); remove.put("$unset", unsets); - _dbSessions.update(mongoKey, remove, false, false, WriteConcern.SAFE); + _dbSessions.updateOne(filterId, remove); return true; } else @@ -315,12 +320,9 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore @Override public boolean doExists(String id) throws Exception { - DBObject fields = new BasicDBObject(); - fields.put(__EXPIRY, 1); - fields.put(__VALID, 1); - fields.put(getContextSubfield(__VERSION), 1); - - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id), fields); + Bson projection = Projections.fields(Projections.include(__ID, __VALID, __EXPIRY, __VERSION, getContextField()), Projections.excludeId()); + Bson filterId = Filters.eq(__ID, id); + Document sessionDocument = _dbSessions.find(filterId).projection(projection).first(); if (sessionDocument == null) return false; //doesn't exist @@ -332,48 +334,33 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore Long expiry = (Long)sessionDocument.get(__EXPIRY); //expired? - if (expiry.longValue() > 0 && expiry.longValue() < System.currentTimeMillis()) + if (expiry != null && expiry > 0 && expiry < System.currentTimeMillis()) return false; //it's expired //does it exist for this context? Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION)); - if (version == null) - return false; - - return true; + return version != null; } @Override public Set doCheckExpired(Set candidates, long time) { - Set expiredSessions = new HashSet<>(); //firstly ask mongo to verify if these candidate ids have expired - all of //these candidates will be for our node - BasicDBObject query = new BasicDBObject(); - query.append(__ID, new BasicDBObject("$in", candidates)); - query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", time)); - - DBCursor verifiedExpiredSessions = null; - try - { - verifiedExpiredSessions = _dbSessions.find(query, new BasicDBObject(__ID, 1)); - for (DBObject session : verifiedExpiredSessions) - { - String id = (String)session.get(__ID); - if (LOG.isDebugEnabled()) - LOG.debug("{} Mongo confirmed expired session {}", _context, id); - expiredSessions.add(id); - } - } - finally - { - if (verifiedExpiredSessions != null) - verifiedExpiredSessions.close(); - } - + Bson query = Filters.and( + Filters.in(__ID, candidates), + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, time)); - //check through sessions that were candidates, but not found as expired. + + FindIterable verifiedExpiredSessions = _dbSessions.find(query); // , new BasicDBObject(__ID, 1) + Set expiredSessions = + StreamSupport.stream(verifiedExpiredSessions.spliterator(), false) + .map(document -> document.getString(__ID)) + .collect(Collectors.toSet()); + + //check through sessions that were candidates, but not found as expired. //they may no longer be persisted, in which case they are treated as expired. for (String c:candidates) { @@ -398,37 +385,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore { // now ask mongo to find sessions for this context, last managed by any // node, that expired before timeLimit - Set expiredSessions = new HashSet<>(); + Bson query = Filters.and( + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, timeLimit) + ); - BasicDBObject query = new BasicDBObject(); - BasicDBObject gt = new BasicDBObject(__EXPIRY, new BasicDBObject("$gt", 0)); - BasicDBObject lt = new BasicDBObject(__EXPIRY, new BasicDBObject("$lte", timeLimit)); - BasicDBList list = new BasicDBList(); - list.add(gt); - list.add(lt); - query.append("$and", list); - - DBCursor oldExpiredSessions = null; - try - { - BasicDBObject bo = new BasicDBObject(__ID, 1); - bo.append(__EXPIRY, 1); - - oldExpiredSessions = _dbSessions.find(query, bo); - for (DBObject session : oldExpiredSessions) - { - String id = (String)session.get(__ID); - - //TODO we should verify if there is a session for my context, not any context - expiredSessions.add(id); - } - } - finally - { - if (oldExpiredSessions != null) - oldExpiredSessions.close(); - } + //TODO we should verify if there is a session for my context, not any context + FindIterable documents = _dbSessions.find(query); + Set expiredSessions = StreamSupport.stream(documents.spliterator(), false) + .map(document -> document.getString(__ID)) + .collect(Collectors.toSet()); return expiredSessions; } @@ -438,9 +405,11 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore //Delete all session documents where the expiry time (which is always the most //up-to-date expiry of all contexts sharing that session id) has already past as //at the timeLimit. - BasicDBObject query = new BasicDBObject(); - query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", timeLimit)); - _dbSessions.remove(query, WriteConcern.SAFE); + Bson query = Filters.and( + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, timeLimit) + ); + _dbSessions.deleteMany(query); } /** @@ -458,8 +427,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore public void doStore(String id, SessionData data, long lastSaveTime) throws Exception { // Form query for upsert - final BasicDBObject key = new BasicDBObject(__ID, id); - + Bson key = Filters.eq(__ID, id);; // Form updates BasicDBObject update = new BasicDBObject(); boolean upsert = false; @@ -487,12 +455,13 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore sets.put(getContextSubfield(__LASTNODE), data.getLastNode()); version = ((Number)version).longValue() + 1L; ((NoSqlSessionData)data).setVersion(version); - update.put("$inc", _version1); + // what is this?? this field is used no where... + //sets.put("$inc", _version1); //if max idle time and/or expiry is smaller for this context, then choose that for the whole session doc BasicDBObject fields = new BasicDBObject(); fields.append(__MAX_IDLE, true); fields.append(__EXPIRY, true); - DBObject o = _dbSessions.findOne(new BasicDBObject("id", id), fields); + Document o = _dbSessions.find(key).first(); if (o != null) { Long tmpLong = (Long)o.get(__MAX_IDLE); @@ -516,37 +485,39 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore try (ByteArrayOutputStream baos = new ByteArrayOutputStream();) { serializeAttributes(data, baos); - sets.put(getContextSubfield(__ATTRIBUTES), baos.toByteArray()); + Binary binary = new Binary(baos.toByteArray()); + sets.put(getContextSubfield(__ATTRIBUTES), binary); } // Do the upsert if (!sets.isEmpty()) update.put("$set", sets); - WriteResult res = _dbSessions.update(key, update, upsert, false, WriteConcern.SAFE); + UpdateResult res = _dbSessions.updateOne(key, update, new UpdateOptions().upsert(upsert)); if (LOG.isDebugEnabled()) LOG.debug("Save:db.sessions.update( {}, {},{} )", key, update, res); } protected void ensureIndexes() throws MongoException { - _version1 = new BasicDBObject(getContextSubfield(__VERSION), 1); - DBObject idKey = BasicDBObjectBuilder.start().add("id", 1).get(); - _dbSessions.createIndex(idKey, - BasicDBObjectBuilder.start() - .add("name", "id_1") - .add("ns", _dbSessions.getFullName()) - .add("sparse", false) - .add("unique", true) - .get()); - - DBObject versionKey = BasicDBObjectBuilder.start().add("id", 1).add("version", 1).get(); - _dbSessions.createIndex(versionKey, BasicDBObjectBuilder.start() - .add("name", "id_1_version_1") - .add("ns", _dbSessions.getFullName()) - .add("sparse", false) - .add("unique", true) - .get()); + var indexes = + StreamSupport.stream(_dbSessions.listIndexes().spliterator(), false) + .toList(); + var indexesNames = indexes.stream().map(document -> document.getString("name")).toList(); + if (!indexesNames.contains("id_1")) + { + String createResult = _dbSessions.createIndex(Indexes.text("id"), + new IndexOptions().unique(true).name("id_1").sparse(false)); + LOG.info("create index {}, result: {}", "id_1", createResult); + } + if (!indexesNames.contains("id_1_version_1")) + { + // Command failed with error 67 (CannotCreateIndex): 'only one text index per collection allowed, found existing text index "id_1"' + String createResult = _dbSessions.createIndex( + Indexes.compoundIndex(Indexes.descending("id"), Indexes.descending("version")), + new IndexOptions().unique(false).name("id_1_version_1").sparse(false)); + LOG.info("create index {}, result: {}", "id_1_version_1", createResult); + } if (LOG.isDebugEnabled()) LOG.debug("Done ensure Mongodb indexes existing"); //TODO perhaps index on expiry time? @@ -587,4 +558,4 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore { return String.format("%s[collection=%s]", super.toString(), getDBCollection()); } -} +} \ No newline at end of file diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java index 14ee7fbe9c8..f1913d9aff2 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.nosql.mongodb; import java.net.UnknownHostException; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.eclipse.jetty.session.AbstractSessionDataStoreFactory; import org.eclipse.jetty.session.SessionDataStore; import org.eclipse.jetty.session.SessionManager; @@ -136,14 +136,14 @@ public class MongoSessionDataStoreFactory extends AbstractSessionDataStoreFactor MongoClient mongo; if (!StringUtil.isBlank(getConnectionString())) - mongo = new MongoClient(new MongoClientURI(getConnectionString())); + mongo = MongoClients.create(getConnectionString()); else if (!StringUtil.isBlank(getHost()) && getPort() != -1) - mongo = new MongoClient(getHost(), getPort()); + mongo = MongoClients.create("mongodb://" + getHost() + ":" + getPort()); else if (!StringUtil.isBlank(getHost())) - mongo = new MongoClient(getHost()); + mongo = MongoClients.create("mongodb://" + getHost()); else - mongo = new MongoClient(); - store.setDBCollection(mongo.getDB(getDbName()).getCollection(getCollectionName())); + mongo = MongoClients.create(); + store.setDBCollection(mongo.getDatabase(getDbName()).getCollection(getCollectionName())); return store; } } diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java index 9d6ecd99b7b..285d0444ddd 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java @@ -22,7 +22,8 @@ import java.util.HashMap; import java.util.Map; import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; +import org.bson.Document; +import org.bson.types.Binary; import org.eclipse.jetty.util.ClassLoadingObjectInputStream; import org.eclipse.jetty.util.URIUtil; @@ -40,6 +41,13 @@ public class MongoUtils { return valueToDecode; } + else if (valueToDecode instanceof Binary) + { + final byte[] decodeObject = ((Binary)valueToDecode).getData(); + final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject); + final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais); + return objectInputStream.readUnshared(); + } else if (valueToDecode instanceof byte[]) { final byte[] decodeObject = (byte[])valueToDecode; @@ -47,13 +55,13 @@ public class MongoUtils final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais); return objectInputStream.readUnshared(); } - else if (valueToDecode instanceof DBObject) + else if (valueToDecode instanceof Document) { - Map map = new HashMap(); - for (String name : ((DBObject)valueToDecode).keySet()) + Map map = new HashMap<>(); + for (String name : ((Document)valueToDecode).keySet()) { String attr = decodeName(name); - map.put(attr, decodeValue(((DBObject)valueToDecode).get(name))); + map.put(attr, decodeValue(((Document)valueToDecode).get(name))); } return map; } @@ -107,19 +115,19 @@ public class MongoUtils /** * Dig through a given dbObject for the nested value * - * @param dbObject the mongo object to search + * @param sessionDocument the mongo document to search * @param nestedKey the field key to find * @return the value of the field key */ - public static Object getNestedValue(DBObject dbObject, String nestedKey) + public static Object getNestedValue(Document sessionDocument, String nestedKey) { String[] keyChain = nestedKey.split("\\."); - DBObject temp = dbObject; + Document temp = sessionDocument; for (int i = 0; i < keyChain.length - 1; ++i) { - temp = (DBObject)temp.get(keyChain[i]); + temp = (Document)temp.get(keyChain[i]); if (temp == null) { diff --git a/pom.xml b/pom.xml index 69bc91da4f7..be4f296e67d 100644 --- a/pom.xml +++ b/pom.xml @@ -373,8 +373,8 @@ 3.9.0 3.4.0 2.2.3 - 3.2.20 - 3.12.14 + 5.0.26 + 5.1.3 4.1.109.Final 0.9.1 8.1.0 @@ -1191,6 +1191,11 @@ mariadb-java-client ${mariadb.version} + + org.mongodb + mongodb-driver-sync + ${mongodb.version} + org.mortbay.jetty.quiche jetty-quiche-native diff --git a/tests/jetty-test-session-common/pom.xml b/tests/jetty-test-session-common/pom.xml index e0099ccd66d..6ce26c7259b 100644 --- a/tests/jetty-test-session-common/pom.xml +++ b/tests/jetty-test-session-common/pom.xml @@ -101,7 +101,7 @@ org.mongodb - mongo-java-driver + mongodb-driver-sync ${mongodb.version} compile diff --git a/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java b/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java index 525859be766..5c3d16d4a65 100644 --- a/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java +++ b/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java @@ -18,13 +18,20 @@ import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.net.UnknownHostException; import java.util.Map; +import java.util.stream.StreamSupport; import com.mongodb.BasicDBObject; -import com.mongodb.DBCollection; import com.mongodb.DBObject; -import com.mongodb.MongoClient; import com.mongodb.MongoException; import com.mongodb.WriteConcern; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import org.bson.Document; +import org.bson.types.Binary; import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStore; import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStoreFactory; import org.eclipse.jetty.nosql.mongodb.MongoUtils; @@ -57,7 +64,7 @@ public class MongoTestHelper static { - mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "3.2.20"))) + mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "5.0.26"))) .withLogConsumer(new Slf4jLogConsumer(MONGO_LOG)); long start = System.currentTimeMillis(); mongo.start(); @@ -65,21 +72,21 @@ public class MongoTestHelper mongoPort = mongo.getMappedPort(MONGO_PORT); LOG.info("Mongo container started for {}:{} - {}ms", mongoHost, mongoPort, System.currentTimeMillis() - start); - mongoClient = new MongoClient(mongoHost, mongoPort); + mongoClient = MongoClients.create(mongo.getConnectionString()); } public static MongoClient getMongoClient() throws UnknownHostException { if (mongoClient == null) { - mongoClient = new MongoClient(mongoHost, mongoPort); + mongoClient = MongoClients.create(mongo.getConnectionString()); } return mongoClient; } public static void dropCollection(String dbName, String collectionName) throws Exception { - getMongoClient().getDB(dbName).getCollection(collectionName).drop(); + getMongoClient().getDatabase(dbName).getCollection(collectionName).withWriteConcern(WriteConcern.JOURNALED).drop(); } public static void shutdown() throws Exception @@ -89,12 +96,14 @@ public class MongoTestHelper public static void createCollection(String dbName, String collectionName) throws UnknownHostException, MongoException { - getMongoClient().getDB(dbName).createCollection(collectionName, null); + if (StreamSupport.stream(getMongoClient().getDatabase(dbName).listCollectionNames().spliterator(), false) + .filter(collectionName::equals).findAny().isEmpty()) + getMongoClient().getDatabase(dbName).withWriteConcern(WriteConcern.JOURNALED).createCollection(collectionName, new CreateCollectionOptions()); } - public static DBCollection getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException + public static MongoCollection getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException { - return getMongoClient().getDB(dbName).getCollection(collectionName); + return getMongoClient().getDatabase(dbName).getCollection(collectionName); } public static MongoSessionDataStoreFactory newSessionDataStoreFactory(String dbName, String collectionName) @@ -108,15 +117,15 @@ public class MongoTestHelper } public static boolean checkSessionExists(String id, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); DBObject fields = new BasicDBObject(); fields.put(MongoSessionDataStore.__EXPIRY, 1); fields.put(MongoSessionDataStore.__VALID, 1); - DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, id), fields); + Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, id)).first(); if (sessionDocument == null) return false; //doesn't exist @@ -125,21 +134,21 @@ public class MongoTestHelper } public static boolean checkSessionPersisted(SessionData data, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); DBObject fields = new BasicDBObject(); - DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, data.getId()), fields); + Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, data.getId())).first(); if (sessionDocument == null) return false; //doesn't exist LOG.debug("{}", sessionDocument); - Boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID); + boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID); - if (valid == null || !valid) + if (!valid) return false; Long created = (Long)sessionDocument.get(MongoSessionDataStore.__CREATED); @@ -149,13 +158,13 @@ public class MongoTestHelper Long expiry = (Long)sessionDocument.get(MongoSessionDataStore.__EXPIRY); Object version = MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__VERSION); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__VERSION); Long lastSaved = (Long)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTSAVED); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTSAVED); String lastNode = (String)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTNODE); - byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTNODE); + byte[] attributes = ((Binary)MongoUtils.getNestedValue(sessionDocument, + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES)).getData(); assertEquals(data.getCreated(), created.longValue()); assertEquals(data.getAccessed(), accessed.longValue()); @@ -167,9 +176,9 @@ public class MongoTestHelper assertNotNull(lastSaved); // get the session for the context - DBObject sessionSubDocumentForContext = - (DBObject)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath()); + Document sessionSubDocumentForContext = + (Document)MongoUtils.getNestedValue(sessionDocument, + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath()); assertNotNull(sessionSubDocumentForContext); @@ -200,12 +209,9 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); - - // Form query for upsert - BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form updates BasicDBObject update = new BasicDBObject(); @@ -236,12 +242,12 @@ public class MongoTestHelper ObjectOutputStream oos = new ObjectOutputStream(baos)) { SessionData.serializeAttributes(tmp, oos); - sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray()); + sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray())); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(Filters.eq(MongoSessionDataStore.__ID, id), update, new UpdateOptions().upsert(true)); } public static void createSession(String id, String contextPath, String vhost, @@ -249,10 +255,10 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -283,12 +289,12 @@ public class MongoTestHelper ObjectOutputStream oos = new ObjectOutputStream(baos)) { SessionData.serializeAttributes(tmp, oos); - sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray()); + sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray())); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(key, update, new UpdateOptions().upsert(true)); } public static void createLegacySession(String id, String contextPath, String vhost, @@ -296,10 +302,10 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { //make old-style session to test if we can retrieve it - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -329,10 +335,10 @@ public class MongoTestHelper { Object value = attributes.get(name); sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoUtils.encodeName(name), - MongoUtils.encodeName(value)); + MongoUtils.encodeName(value)); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(key, update, new UpdateOptions().upsert(true)); } -} +} \ No newline at end of file diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java index a4c0a17dbf7..7c54f9dd726 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java @@ -35,7 +35,7 @@ public class MongodbSessionDistributionTests extends AbstractSessionDistribution private static final int MONGO_PORT = 27017; - final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "3.2.20"); + final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "5.0.26"); final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse(imageName))