diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 3e7e580829..76b9ee317d 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -739,6 +739,7 @@ src/main/java/org/exist/dom/memtree/reference/ElementReferenceImpl.java src/main/java/org/exist/dom/memtree/reference/ProcessingInstructionReferenceImpl.java src/main/java/org/exist/dom/memtree/reference/TextReferenceImpl.java + src/test/java/org/exist/http/urlrewrite/RedirectTest.java src/main/java/org/exist/util/ByteOrderMark.java src/main/java/org/exist/util/JREUtil.java src/main/java/org/exist/util/OSUtil.java @@ -1087,6 +1088,7 @@ src/main/java/org/exist/http/urlrewrite/ModuleCall.java src/main/java/org/exist/http/urlrewrite/PathForward.java src/main/java/org/exist/http/urlrewrite/Redirect.java + src/test/java/org/exist/http/urlrewrite/RedirectTest.java src/main/java/org/exist/http/urlrewrite/RewriteConfig.java src/main/java/org/exist/indexing/Index.java src/main/java/org/exist/indexing/IndexManager.java diff --git a/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java b/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java index 533500a884..e063325afa 100644 --- a/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java +++ b/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java @@ -51,12 +51,19 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + +import javax.annotation.Nullable; import java.io.IOException; +import static org.exist.util.StringUtil.isNullOrEmpty; + public class Redirect extends URLRewrite { + private @Nullable RedirectType redirectType; + public Redirect(final Element config, final String uri) throws ServletException { super(config, uri); + this.redirectType = parseRedirectType(config.getAttribute("type")); final String redirectTo = config.getAttribute("url"); if (redirectTo.isEmpty()) { throw new ServletException(" needs an attribute 'url'."); @@ -71,16 +78,66 @@ public Redirect(final Element config, final String uri) throws ServletException public Redirect(final Redirect other) { super(other); + this.redirectType = other.redirectType; } @Override public void doRewrite(final HttpServletRequest request, final HttpServletResponse response) throws IOException { - setHeaders(new HttpResponseWrapper(response)); - response.sendRedirect(target); + if (redirectType == null) { + redirectType = "GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod()) ? RedirectType.Found : RedirectType.SeeOther; + } + + final HttpResponseWrapper responseWrapper = new HttpResponseWrapper(response); + setHeaders(responseWrapper); + responseWrapper.setStatusCode(redirectType.httpStatusCode); + responseWrapper.setHeader("Location", target); + + // commit the response + responseWrapper.flushBuffer(); } @Override protected URLRewrite copy() { return new Redirect(this); } + + private static @Nullable RedirectType parseRedirectType(@Nullable final String strRedirectType) throws ServletException { + // first, if no value use the default + if (isNullOrEmpty(strRedirectType)) { + return null; + } + + // second, try to parse by number + try { + final int intRedirectType = Integer.valueOf(strRedirectType); + for (final RedirectType redirectType : RedirectType.values()) { + if (redirectType.httpStatusCode == intRedirectType) { + return redirectType; + } + } + } catch (final NumberFormatException e) { + // ignore - no op + } + + // third, try to parse by name + try { + return RedirectType.valueOf(strRedirectType); + } catch (final IllegalArgumentException e) { + throw new ServletException(" the return type of the fn function. * @param fn the function which accepts the HTTP Client. @@ -105,16 +105,40 @@ protected static String getAppsUri(final ExistWebServer existWebServer) { * @throws IOException if an I/O error occurs */ protected static T withHttpClient(final FunctionE fn) throws IOException { - try (final CloseableHttpClient client = HttpClientBuilder - .create() - .disableAutomaticRetries() - .build()) { + return withHttpClient(false, fn); + } + + /** + * Execute a function with an HTTP Client. + * + * @param the return type of the fn function. + * @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default). + * @param fn the function which accepts the HTTP Client. + * + * @return the result of the fn function. + * + * @throws IOException if an I/O error occurs + */ + protected static T withHttpClient(final boolean disableRedirectHandling, final FunctionE fn) throws IOException { + try (final CloseableHttpClient client = buildHttpClient(disableRedirectHandling)) { return fn.apply(client); } } + private static CloseableHttpClient buildHttpClient(final boolean disableRedirectHandling) { + HttpClientBuilder builder = HttpClientBuilder + .create() + .disableAutomaticRetries(); + + if (disableRedirectHandling) { + builder = builder.disableRedirectHandling(); + } + + return builder.build(); + } + /** - * Execute a function with a HTTP Executor. + * Execute a function with an HTTP Executor. * * @param the return type of the fn function. * @param existWebServer the Web Server. @@ -125,11 +149,27 @@ protected static T withHttpClient(final FunctionE T withHttpExecutor(final ExistWebServer existWebServer, final FunctionE fn) throws IOException { - return withHttpClient(client -> { + return withHttpExecutor(existWebServer, false, fn); + } + + /** + * Execute a function with an HTTP Executor. + * + * @param the return type of the fn function. + * @param existWebServer the Web Server. + * @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default). + * @param fn the function which accepts the HTTP Executor. + * + * @return the result of the fn function. + * + * @throws IOException if an I/O error occurs + */ + protected static T withHttpExecutor(final ExistWebServer existWebServer, final boolean disableRedirectHandling, final FunctionE fn) throws IOException { + return withHttpClient(disableRedirectHandling, client -> { final Executor executor = Executor - .newInstance(client) - .auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD) - .authPreemptive(new HttpHost("localhost", existWebServer.getPort())); + .newInstance(client) + .auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD) + .authPreemptive(new HttpHost("localhost", existWebServer.getPort())); return fn.apply(executor); }); } diff --git a/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java b/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java new file mode 100644 index 0000000000..e1ebe3a79a --- /dev/null +++ b/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java @@ -0,0 +1,154 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.http.urlrewrite; + +import com.evolvedbinary.j8fu.tuple.Tuple2; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.fluent.Request; +import org.apache.http.entity.ContentType; +import org.exist.http.AbstractHttpTest; +import org.exist.test.ExistWebServer; +import org.exist.xmldb.XmldbURI; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.function.Function; + +import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; +import static org.exist.http.urlrewrite.XQueryURLRewrite.XQUERY_CONTROLLER_FILENAME; +import static org.junit.Assert.assertEquals; + +/** + * @author Adam Retter + */ +public class RedirectTest extends AbstractHttpTest { + + private static final XmldbURI TEST_COLLECTION_NAME = XmldbURI.create("redirect-test"); + private static final XmldbURI TEST_COLLECTION = XmldbURI.create("/db/apps").append(TEST_COLLECTION_NAME); + + private static final String TEST_CONTROLLER = + "xquery version \"1.0\";\n" + + "declare namespace exist = \"http://exist.sourceforge.net/NS/exist\";\n" + + "declare namespace request = \"http://exist-db.org/xquery/request\";\n" + + "let $redirect-type := request:get-parameter(\"redirect-type\", ())\n" + + "return\n" + + " element exist:dispatch {\n" + + " element exist:redirect {\n" + + " attribute url { \"http://elsewhere.dom\" },\n" + + " $redirect-type ! attribute type { . }\n" + + " }\n" + + " }\n"; + + @ClassRule + public static final ExistWebServer EXIST_WEB_SERVER = new ExistWebServer(true, false, true, true, false); + + @Test + public void defaultForGetIsFound() throws IOException { + testRedirect(Redirect.RedirectType.Found, null, Request::Get); + } + + @Test + public void defaultForHeadIsFound() throws IOException { + testRedirect(Redirect.RedirectType.Found, null, Request::Head); + } + + @Test + public void defaultForPostIsSeeOther() throws IOException { + testRedirect(Redirect.RedirectType.SeeOther, null, Request::Post); + } + + @Test + public void defaultForPatchIsSeeOther() throws IOException { + testRedirect(Redirect.RedirectType.SeeOther, null, Request::Patch); + } + + @Test + public void defaultForPutIsSeeOther() throws IOException { + testRedirect(Redirect.RedirectType.SeeOther, null, Request::Put); + } + + @Test + public void defaultForDeleteIsSeeOther() throws IOException { + testRedirect(Redirect.RedirectType.SeeOther, null, Request::Delete); + } + + @Test + public void movedPermanently() throws IOException { + testGetSendRedirect(Redirect.RedirectType.MovedPermanently); + } + + @Test + public void found() throws IOException { + testGetSendRedirect(Redirect.RedirectType.Found); + } + + @Test + public void seeOther() throws IOException { + testGetSendRedirect(Redirect.RedirectType.SeeOther); + } + + @Test + public void temporaryRedirect() throws IOException { + testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect); + } + + @Test + public void permanentRedirect() throws IOException { + testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect); + } + + private void testGetSendRedirect(final Redirect.RedirectType redirectType) throws IOException { + testGetRedirect(redirectType, redirectType); + } + + private void testGetRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType) throws IOException { + testRedirect(expectedRedirectTypeResponse, sendRedirectType, Request::Get); + } + + private void testRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType, final Function requestFn) throws IOException { + final String uri = getAppsUri(EXIST_WEB_SERVER) + "/" + TEST_COLLECTION_NAME.append("anything") + (sendRedirectType != null ? "?redirect-type=" + sendRedirectType.httpStatusCode : ""); + final Request request = requestFn.apply(uri); + final Tuple2 redirectStatusCodeAndLocation = withHttpExecutor(EXIST_WEB_SERVER, true, executor -> { + final HttpResponse response = executor.execute(request).returnResponse(); + return Tuple(response.getStatusLine().getStatusCode(), response.getFirstHeader("Location").getValue()); + }); + assertEquals(expectedRedirectTypeResponse.httpStatusCode, redirectStatusCodeAndLocation._1.intValue()); + assertEquals("http://elsewhere.dom", redirectStatusCodeAndLocation._2); + } + + + @BeforeClass + public static void setup() throws IOException { + final Request request = Request + .Put(getRestUri(EXIST_WEB_SERVER) + TEST_COLLECTION + "/" + XQUERY_CONTROLLER_FILENAME) + .bodyString(TEST_CONTROLLER, ContentType.create("application/xquery")); + + final int statusCode = withHttpExecutor(EXIST_WEB_SERVER, executor -> + executor.execute(request).returnResponse().getStatusLine().getStatusCode() + ); + + assertEquals(HttpStatus.SC_CREATED, statusCode); + } +}