diff --git a/core/src/main/java/io/undertow/server/handlers/ReasonPhraseHandler.java b/core/src/main/java/io/undertow/server/handlers/ReasonPhraseHandler.java new file mode 100644 index 0000000000..7ea9655a9f --- /dev/null +++ b/core/src/main/java/io/undertow/server/handlers/ReasonPhraseHandler.java @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * A handler which simply sets a response code. + * + * @author Bartosz Baranowski + */ +public final class ReasonPhraseHandler implements HttpHandler { + + private static final boolean debugEnabled; + + static { + debugEnabled = UndertowLogger.PREDICATE_LOGGER.isDebugEnabled(); + } + + private final String reasonPhrase; + + private final HttpHandler next; + /** + * Construct a new instance. + * + * @param reasonPhrase the reason phrase to be set in status line + */ + public ReasonPhraseHandler(final HttpHandler next, final String reasonPhrase) { + this.next = next; + this.reasonPhrase = reasonPhrase; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.setReasonPhrase(reasonPhrase); + if(debugEnabled) { + UndertowLogger.PREDICATE_LOGGER.debugf("Reason phrase set to [%s] for %s.", this.reasonPhrase, exchange); + } + if(next != null) { + next.handleRequest(exchange); + } + } + + @Override + public String toString() { + return "reason-phrase( " + this.reasonPhrase + " )"; + } +} diff --git a/core/src/main/java/io/undertow/server/handlers/ResponseCodeHandler.java b/core/src/main/java/io/undertow/server/handlers/ResponseCodeHandler.java index e4b4a20591..19f43354ad 100644 --- a/core/src/main/java/io/undertow/server/handlers/ResponseCodeHandler.java +++ b/core/src/main/java/io/undertow/server/handlers/ResponseCodeHandler.java @@ -64,13 +64,36 @@ public final class ResponseCodeHandler implements HttpHandler { private final int responseCode; + private HttpHandler next; /** * Construct a new instance. * * @param responseCode the response code to set + * @param next next handler */ - public ResponseCodeHandler(final int responseCode) { + public ResponseCodeHandler(final HttpHandler next, final int responseCode) { + assert responseCode > 99; + assert responseCode < 600; this.responseCode = responseCode; + this.next = next; + } + + /** + * Construct a new instance. + * + * @param responseCode the response code to set + * @param next next handler + */ + public ResponseCodeHandler(final int responseCode) { + this(null,responseCode); + } + + public HttpHandler getNext() { + return next; + } + + public void setNext(HttpHandler next) { + this.next = next; } @Override @@ -79,6 +102,9 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { if(debugEnabled) { UndertowLogger.PREDICATE_LOGGER.debugf("Response code set to [%s] for %s.", responseCode, exchange); } + if(next != null) { + next.handleRequest(exchange); + } } @Override diff --git a/core/src/main/java/io/undertow/server/handlers/ResponseHandler.java b/core/src/main/java/io/undertow/server/handlers/ResponseHandler.java new file mode 100644 index 0000000000..1222818bf1 --- /dev/null +++ b/core/src/main/java/io/undertow/server/handlers/ResponseHandler.java @@ -0,0 +1,108 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.undertow.server.handlers; + +import io.undertow.UndertowLogger; +import io.undertow.server.BlockingHttpExchange; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; + +/** + * Class which handles set operations for response: code, reason phrase and potentially body and type. Status code is required + * parameter.
+ * The response handler allows to set response body as well.
+ * response(code=404, reason='dont like it') is roughly equivalent to reason-phrase('dont like it');response-code(404)"
+ * + * @author Bartosz Baranowski + */ +public class ResponseHandler implements HttpHandler { + + private static final String DEFAULT_BODY_TYPE = "text/html"; + private static final boolean debugEnabled; + + static { + debugEnabled = UndertowLogger.PREDICATE_LOGGER.isDebugEnabled(); + } + + private final String body; + private final String type; + private final int code; + private final String reason; + private HttpHandler chained; + + // TODO: review parsing/execution rules. For some reason without next, this particular handler does not ignore + // trailing handlers. + public ResponseHandler(final int code, final String reason) { + this(code, reason, null, null); + } + + public ResponseHandler(final int code, final String reason, final String body) { + this(code, reason, body, DEFAULT_BODY_TYPE); + } + + public ResponseHandler(final int code, final String reason, final String body, final String type) { + this.body = body; + this.type = type; + // toString only + this.code = code; + this.reason = reason; + if (reason != null) { + this.chained = new ReasonPhraseHandler(null, reason); + } + this.chained = new ResponseCodeHandler(this.chained, code); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + this.chained.handleRequest(exchange); + if (this.body != null) { + final byte[] bodyBytes = this.body.getBytes("UTF-8"); + final HeaderMap responseHeaders = exchange.getResponseHeaders(); + if (responseHeaders.contains(Headers.CONTENT_LENGTH) || responseHeaders.contains(Headers.CONTENT_TYPE) || exchange.isBlocking()) { + // TODO: explode + } + responseHeaders.add(Headers.CONTENT_TYPE, this.type); + responseHeaders.add(Headers.CONTENT_LENGTH, bodyBytes.length); + final BlockingHttpExchange blockingHttpExchange = exchange.startBlocking(); + if (exchange.isInIoThread()) { + exchange.dispatch(new HttpHandler() { + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getOutputStream().write(bodyBytes); + } + }); + } else { + exchange.getOutputStream().write(bodyBytes); + } + + if (debugEnabled) { + UndertowLogger.PREDICATE_LOGGER.debugf("Respons body set to \n[%s]\nfor %s.", this.body, exchange); + } + } + } + + @Override + public String toString() { + return "response( code='" + code + "'" + ((this.reason != null) ? ", reason='" + this.reason + "'" : "") + "" + + ((this.body != null) ? ", type='" + this.type + "', body='" + this.body + "'" : "") + " )"; + } + +} diff --git a/core/src/main/java/io/undertow/server/handlers/builder/ReasonPhraseHandlerBuilder.java b/core/src/main/java/io/undertow/server/handlers/builder/ReasonPhraseHandlerBuilder.java new file mode 100644 index 0000000000..b0ea220131 --- /dev/null +++ b/core/src/main/java/io/undertow/server/handlers/builder/ReasonPhraseHandlerBuilder.java @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers.builder; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.ReasonPhraseHandler; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Bartosz Baranowski + */ +public class ReasonPhraseHandlerBuilder implements HandlerBuilder { + @Override + public String name() { + return "reason-phrase"; + } + + @Override + public Map> parameters() { + Map> parameters = new HashMap<>(); + parameters.put("value", String.class); + return parameters; + } + + @Override + public Set requiredParameters() { + final Set req = new HashSet<>(); + req.add("value"); + return req; + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public HandlerWrapper build(final Map config) { + final String value = (String) config.get("value"); + return new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new ReasonPhraseHandler(handler, value); + } + }; + } +} diff --git a/core/src/main/java/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java b/core/src/main/java/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java index ac6631cba2..1ae2628a8a 100644 --- a/core/src/main/java/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java +++ b/core/src/main/java/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java @@ -61,7 +61,7 @@ public HandlerWrapper build(final Map config) { return new HandlerWrapper() { @Override public HttpHandler wrap(HttpHandler handler) { - return new ResponseCodeHandler(value); + return new ResponseCodeHandler(handler, value); } }; } diff --git a/core/src/main/java/io/undertow/server/handlers/builder/ResponseHandlerBuilder.java b/core/src/main/java/io/undertow/server/handlers/builder/ResponseHandlerBuilder.java new file mode 100644 index 0000000000..111f31d9f2 --- /dev/null +++ b/core/src/main/java/io/undertow/server/handlers/builder/ResponseHandlerBuilder.java @@ -0,0 +1,83 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers.builder; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.ResponseHandler; + +/** + * @author Bartosz Baranowski + */ +public class ResponseHandlerBuilder implements HandlerBuilder { + @Override + public String name() { + return "response"; + } + + @Override + public Map> parameters() { + Map> parameters = new HashMap<>(); + parameters.put("code", Integer.class); + parameters.put("reason", String.class); + parameters.put("type", String.class); + parameters.put("body", String.class); + return parameters; + } + + @Override + public Set requiredParameters() { + final Set req = new HashSet<>(); + req.add("code"); + return req; + } + + @Override + public String defaultParameter() { + // default parameter - name(paramValue) not supported + return null; + } + + @Override + public HandlerWrapper build(final Map config) { + final Integer code = (Integer) config.get("code"); + final String reason = (String) config.get("reason"); + final String type = (String) config.get("type"); + final String body = (String) config.get("body"); + return new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + if (body == null) { + return new ResponseHandler(code, reason); + } else { + if (type == null) { + return new ResponseHandler(code, reason, body); + } else { + return new ResponseHandler(code, reason, body, type); + } + } + } + }; + } +} diff --git a/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder b/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder index 0c2affdbe4..70f45c59c5 100644 --- a/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder +++ b/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder @@ -43,3 +43,5 @@ io.undertow.server.handlers.HttpContinueAcceptingHandler$Builder io.undertow.server.handlers.form.EagerFormParsingHandler$Builder io.undertow.server.handlers.SameSiteCookieHandler$Builder io.undertow.server.handlers.SetErrorHandler$Builder +io.undertow.server.handlers.builder.ReasonPhraseHandlerBuilder +io.undertow.server.handlers.builder.ResponseHandlerBuilder diff --git a/core/src/test/java/io/undertow/predicate/PredicateParsingTestCase.java b/core/src/test/java/io/undertow/predicate/PredicateParsingTestCase.java index a64be6a14f..5d8cb1f01b 100644 --- a/core/src/test/java/io/undertow/predicate/PredicateParsingTestCase.java +++ b/core/src/test/java/io/undertow/predicate/PredicateParsingTestCase.java @@ -141,4 +141,5 @@ private void expect(String string, boolean result1, boolean result2) { throw new RuntimeException("String " + string, ex); } } + } diff --git a/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java b/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java index 7ded4cd9d2..4cc438e617 100644 --- a/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java +++ b/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java @@ -25,8 +25,11 @@ import io.undertow.server.handlers.builder.PredicatedHandlersParser; import io.undertow.testutils.DefaultServer; import io.undertow.testutils.HttpClientUtils; +import io.undertow.testutils.ProxyIgnore; import io.undertow.testutils.TestHttpClient; import io.undertow.util.StatusCodes; + +import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.junit.Assert; @@ -159,4 +162,100 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { handler.handleRequest(e); Assert.assertEquals("foo'bar",e.getResponseHeaders().get("test-header").getFirst()); } + + @Test @ProxyIgnore + public void testReasonPhrase() throws IOException { + DefaultServer.setRootHandler( + Handlers.predicates( + + PredicatedHandlersParser.parse( + "path('/test') -> reason-phrase('test-my-patience');response-code(480)", getClass().getClassLoader()), new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send(exchange.getRelativePath()); + } + })); + TestHttpClient client = new TestHttpClient(); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/test"); + + HttpResponse result = client.execute(get); + Assert.assertEquals("test-my-patience", result.getStatusLine().getReasonPhrase()); + Assert.assertEquals(480, result.getStatusLine().getStatusCode()); + + } finally { + client.getConnectionManager().shutdown(); + } + + } + + @Test @ProxyIgnore + public void testDefaultResponse() throws IOException { + DefaultServer.setRootHandler( + Handlers.predicates( + PredicatedHandlersParser.parse( + "path('/test') -> response(code='208', reason='test-my-patience', body='Dont-Touch')", getClass().getClassLoader()), new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send(exchange.getRelativePath()); + } + })); + TestHttpClient client = new TestHttpClient(); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/test"); + + HttpResponse result = client.execute(get); + Assert.assertEquals("test-my-patience", result.getStatusLine().getReasonPhrase()); + Assert.assertEquals(208, result.getStatusLine().getStatusCode()); + final String body = HttpClientUtils.readResponse(result); + Assert.assertEquals("Dont-Touch", body); + Header[] hdrs = result.getHeaders("Content-Length"); + Assert.assertNotNull(hdrs); + Assert.assertEquals(1, hdrs.length); + Assert.assertNotNull(hdrs[0]); + Assert.assertEquals(hdrs[0].getValue(), ""+"Dont-Touch".length()); + hdrs = result.getHeaders("Content-Type"); + Assert.assertNotNull(hdrs); + Assert.assertEquals(1, hdrs.length); + Assert.assertNotNull(hdrs[0]); + Assert.assertEquals(hdrs[0].getValue(), "text/html"); + } finally { + client.getConnectionManager().shutdown(); + } + } + + @Test @ProxyIgnore + public void testCustomTypeResponse() throws IOException { + DefaultServer.setRootHandler( + Handlers.predicates( + PredicatedHandlersParser.parse( + "path('/test') -> response(code='208', reason='test-my-patience', body='Dont-Touch', type='text/plain')", getClass().getClassLoader()), new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send(exchange.getRelativePath()); + } + })); + TestHttpClient client = new TestHttpClient(); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/test"); + + HttpResponse result = client.execute(get); + Assert.assertEquals("test-my-patience", result.getStatusLine().getReasonPhrase()); + Assert.assertEquals(208, result.getStatusLine().getStatusCode()); + final String body = HttpClientUtils.readResponse(result); + Assert.assertEquals("Dont-Touch", body); + Header[] hdrs = result.getHeaders("Content-Length"); + Assert.assertNotNull(hdrs); + Assert.assertEquals(1, hdrs.length); + Assert.assertNotNull(hdrs[0]); + Assert.assertEquals(hdrs[0].getValue(), ""+"Dont-Touch".length()); + hdrs = result.getHeaders("Content-Type"); + Assert.assertNotNull(hdrs); + Assert.assertEquals(1, hdrs.length); + Assert.assertNotNull(hdrs[0]); + Assert.assertEquals(hdrs[0].getValue(), "text/plain"); + } finally { + client.getConnectionManager().shutdown(); + } + } }