diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandler.java b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java similarity index 64% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandler.java rename to common/http/src/main/java/io/helidon/common/http/DirectHandler.java index 5f634289c0d..cc2f97e762d 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandler.java +++ b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,82 +14,117 @@ * limitations under the License. */ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * 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.helidon.nima.webserver.http; +package io.helidon.common.http; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Optional; -import io.helidon.common.buffers.BufferData; -import io.helidon.common.http.HeadersServerResponse; -import io.helidon.common.http.Http; -import io.helidon.common.http.Http.HeaderName; - /** - * A handler that is invoked when a response is sent outside of router. - * See {@link SimpleHandler.EventType} to see which types + * A handler that is invoked when a response is sent outside of routing. + * See {@link DirectHandler.EventType} to see which types * of events are covered by this handler. + * Direct handlers can be used both with blocking and reactive servers in Helidon. */ @FunctionalInterface -public interface SimpleHandler { +public interface DirectHandler { + /** + * Default handler will HTML encode the message (if any), + * use the default status code for the event type, and copy all headers configured. + * + * + * @return default direct handler + */ + static DirectHandler defaultHandler() { + return DirectHandlerDefault.INSTANCE; + } + /** * Handler of responses that bypass router. *

* This method should be used to return custom status, header and possible entity. * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. * - * @param request request as received with as much known information as possible - * @param eventType type of the event + * @param request request as received with as much known information as possible + * @param eventType type of the event * @param defaultStatus default status expected to be returned * @param responseHeaders headers to be added to response - * @param thrown throwable caught as part of processing with possible additional details about the reason of failure + * @param thrown throwable caught as part of processing with possible additional details about the reason of failure * @return response to use to return to original request */ - default SimpleResponse handle(SimpleRequest request, - EventType eventType, - Http.Status defaultStatus, - HeadersServerResponse responseHeaders, - Throwable thrown) { + default TransportResponse handle(TransportRequest request, + EventType eventType, + Http.Status defaultStatus, + HeadersServerResponse responseHeaders, + Throwable thrown) { return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage()); } /** - * Handler of responses that bypass router. + * Handler of responses that bypass routing. *

* This method should be used to return custom status, header and possible entity. * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. * - * @param request request as received with as much known information as possible - * @param eventType type of the event + * @param request request as received with as much known information as possible + * @param eventType type of the event * @param defaultStatus default status expected to be returned * @param responseHeaders headers to be added to response - * @param message informative message for cases that are not triggered by an exception, by default this will be called + * @param message informative message for cases that are not triggered by an exception, by default this will be called * also * for exceptional cases with the exception message * @return response to use to return to original request */ - SimpleResponse handle(SimpleRequest request, - EventType eventType, - Http.Status defaultStatus, - HeadersServerResponse responseHeaders, - String message); + TransportResponse handle(TransportRequest request, + EventType eventType, + Http.Status defaultStatus, + HeadersServerResponse responseHeaders, + String message); + + /** + * Request information. + * Note that the information may not be according to specification, as this marks a bad request (by definition). + */ + interface TransportRequest { + /** + * Create an empty transport request. + * This is usually used when an error occurs before we could parse request information. + * + * @return empty transport request + */ + static TransportRequest empty() { + return DirectHandlerEmptyRequest.INSTANCE; + } + + /** + * Protocol version (either from actual request, or guessed). + * + * @return protocol version + */ + String protocolVersion(); + + /** + * HTTP method. + * + * @return method + */ + String method(); + + /** + * Requested path, if found in request. + * + * @return uri or an empty string + */ + String path(); + + /** + * Headers, if found in request. + * + * @return headers or an empty map + */ + HeadersServerRequest headers(); + } /** * Types of events that can be triggered outside of router @@ -149,82 +184,19 @@ public boolean keepAlive() { } } - /** - * Request information. - * Note that the information may not be according to specification, as this marks a bad request (by definition). - */ - interface SimpleRequest { - /** - * Empty request, for cases where no information is available. - * - * @return empty simple request - */ - static SimpleRequest empty() { - return new SimpleRequest() { - @Override - public String protocolVersion() { - return ""; - } - - @Override - public String method() { - return ""; - } - - @Override - public String path() { - return ""; - } - - @Override - public Map> headers() { - return Map.of(); - } - }; - } - - /** - * Protocol version (either from actual request, or guessed). - * - * @return protocol version - */ - String protocolVersion(); - - /** - * HTTP method. - * - * @return method - */ - String method(); - - /** - * Requested URI, if found in request. - * - * @return uri or an empty string - */ - String path(); - - /** - * Headers, if found in request. - * - * @return headers or an empty map - */ - Map> headers(); - } - /** * Response to correctly reply to the original client. */ - class SimpleResponse { + class TransportResponse { private final Http.Status status; - private final byte[] message; private final HeadersServerResponse headers; + private final byte[] entity; private final boolean keepAlive; - private SimpleResponse(Builder builder) { + private TransportResponse(Builder builder) { this.status = builder.status; - this.message = builder.message; this.headers = builder.headers; + this.entity = builder.entity; this.keepAlive = builder.keepAlive; } @@ -260,8 +232,8 @@ public HeadersServerResponse headers() { * * @return mesage bytes or empty if no message is configured */ - public Optional message() { - return Optional.ofNullable(message); + public Optional entity() { + return Optional.ofNullable(entity); } /** @@ -274,11 +246,11 @@ public boolean keepAlive() { } /** - * Fluent API builder for {@link SimpleHandler.SimpleResponse}. + * Fluent API builder for {@link DirectHandler.TransportResponse}. */ - public static class Builder implements io.helidon.common.Builder { - private Http.Status status = Http.Status.OK_200; - private byte[] message = BufferData.EMPTY_BYTES; + public static class Builder implements io.helidon.common.Builder { + private Http.Status status = Http.Status.BAD_REQUEST_400; + private byte[] entity; private HeadersServerResponse headers = HeadersServerResponse.create(); private boolean keepAlive = true; @@ -286,8 +258,8 @@ private Builder() { } @Override - public SimpleResponse build() { - return new SimpleResponse(this); + public TransportResponse build() { + return new TransportResponse(this); } /** @@ -313,64 +285,73 @@ public Builder headers(HeadersServerResponse headers) { } /** - * Configure keep alive. + * Set a header (if exists, it would be replaced). + * Keep alive header is ignored, please use {@link #keepAlive(boolean)}. * - * @param keepAlive whether to keep alive + * @param name name of the header + * @param values value of the header * @return updated builder */ - public Builder keepAlive(boolean keepAlive) { - this.keepAlive = keepAlive; + public Builder header(Http.HeaderName name, String... values) { + this.headers.set(name, List.of(values)); return this; } /** - * Custom entity. Uses the content, encodes it for HTML, reads it as {@code UTF-8}, configures - * {@code Content-Length} header, configures {@code Content-Type} header to {@code text/plain}. - *

- * Use {@link #message(byte[])} for custom encoding. + * Set a header (if exists, it would be replaced). + * Keep alive header is ignored, please use {@link #keepAlive(boolean)}. * - * @param message response entity + * @param header header value * @return updated builder */ - public Builder message(String message) { - this.headers.setIfAbsent(Http.HeaderValues.CONTENT_TYPE_TEXT_PLAIN); - return message(message.getBytes(StandardCharsets.UTF_8)); + public Builder header(Http.HeaderValue header) { + this.headers.add(header); + return this; } /** - * Custom entity. Uses the content, configures - * {@code Content-Length} header. - *

- * Use {@link #message(String)} for simple text messages. + * Configure keep alive. * - * @param entity response entity + * @param keepAlive whether to keep alive * @return updated builder */ - public Builder message(byte[] entity) { - this.message = entity; + public Builder keepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; return this; } /** - * Configure an additional header. + * Custom entity. Uses the content, reads it as {@code UTF-8}, configures + * {@code Content-Length} header, configures {@code Content-Type} header to {@code text/plain}. + *

+ * Use {@link #entity(byte[])} for custom encoding. + *

+ * Note that this method does not do any escaping (such as HTML encoding), make sure the entity is safe. * - * @param headerName header name - * @param headerValue header value + * @param entity response entity * @return updated builder */ - public Builder header(HeaderName headerName, String headerValue) { - this.headers.add(headerName.withValue(headerValue)); - return this; + public Builder entity(String entity) { + this.headers.setIfAbsent(Http.HeaderValues.CONTENT_TYPE_TEXT_PLAIN); + return entity(entity.getBytes(StandardCharsets.UTF_8)); } /** - * Configure an additional header. + * Custom entity. Uses the content, configures + * {@code Content-Length} header. + *

+ * Use {@link #entity(String)} for simple text messages. * - * @param header header value + * @param entity response entity * @return updated builder */ - public Builder header(Http.HeaderValue header) { - this.headers.add(header); + public Builder entity(byte[] entity) { + this.entity = Arrays.copyOf(entity, entity.length); + if (this.entity.length == 0) { + this.headers.remove(Http.Header.CONTENT_LENGTH); + } else { + header(Http.Header.CONTENT_LENGTH, String.valueOf(entity.length)); + } return this; } } diff --git a/common/http/src/main/java/io/helidon/common/http/DirectHandlerDefault.java b/common/http/src/main/java/io/helidon/common/http/DirectHandlerDefault.java new file mode 100644 index 00000000000..fe527db39c8 --- /dev/null +++ b/common/http/src/main/java/io/helidon/common/http/DirectHandlerDefault.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.common.http; + +final class DirectHandlerDefault implements DirectHandler { + static final DirectHandler INSTANCE = new DirectHandlerDefault(); + + private DirectHandlerDefault() { + } + + @Override + public TransportResponse handle(TransportRequest request, + EventType eventType, + Http.Status defaultStatus, + HeadersServerResponse headers, + String message) { + return TransportResponse.builder() + .status(defaultStatus) + .headers(headers) + .update(it -> { + if (message != null && !message.isEmpty()) { + it.entity(HtmlEncoder.encode(message)); + } + }) + .build(); + } +} diff --git a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/HtmlEncoderTest.java b/common/http/src/main/java/io/helidon/common/http/DirectHandlerEmptyRequest.java similarity index 51% rename from reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/HtmlEncoderTest.java rename to common/http/src/main/java/io/helidon/common/http/DirectHandlerEmptyRequest.java index 16482644ff5..c73c65c0fd3 100644 --- a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/HtmlEncoderTest.java +++ b/common/http/src/main/java/io/helidon/common/http/DirectHandlerEmptyRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.reactive.webserver; -import org.junit.jupiter.api.Test; +package io.helidon.common.http; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -class HtmlEncoderTest { - - @Test - void testEncode() { - String s = HtmlEncoder.encode("&<>\"'"); - assertThat(s, is("&<>"'")); - s = HtmlEncoder.encode(""); - assertThat(s, is("<script>bad</script>")); - } +record DirectHandlerEmptyRequest(String protocolVersion, String method, String path, HeadersServerRequest headers) + implements DirectHandler.TransportRequest { + static final DirectHandlerEmptyRequest INSTANCE = + new DirectHandlerEmptyRequest("", + "", + "", + HeadersServerRequest.create(HeadersWritable.create())); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/HtmlEncoder.java b/common/http/src/main/java/io/helidon/common/http/HtmlEncoder.java similarity index 98% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/HtmlEncoder.java rename to common/http/src/main/java/io/helidon/common/http/HtmlEncoder.java index 2933652827b..6fcf390f0dc 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/HtmlEncoder.java +++ b/common/http/src/main/java/io/helidon/common/http/HtmlEncoder.java @@ -30,7 +30,7 @@ * limitations under the License. */ -package io.helidon.nima.webserver; +package io.helidon.common.http; /** * HTML encoding of special characters to prevent cross site scripting (XSS) attacks. diff --git a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/HtmlEncoderTest.java b/common/http/src/test/java/io/helidon/common/http/HtmlEncoderTest.java similarity index 96% rename from nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/HtmlEncoderTest.java rename to common/http/src/test/java/io/helidon/common/http/HtmlEncoderTest.java index f6c74f96486..7e4566b6f99 100644 --- a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/HtmlEncoderTest.java +++ b/common/http/src/test/java/io/helidon/common/http/HtmlEncoderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.nima.webserver; +package io.helidon.common.http; import org.junit.jupiter.api.Test; diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index 8fc9f0f95c5..25506492cae 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -24,6 +24,7 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.buffers.BufferData; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Headers; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http.Header; @@ -52,7 +53,6 @@ import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRouting; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; /** * Server HTTP/2 stream implementation. @@ -272,17 +272,17 @@ public void run() { } catch (Throwable e) { throw HttpException.builder() .message("Internal error") - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(e) .build(); } } catch (HttpException e) { - SimpleHandler handler = ctx.simpleHandlers().handler(e.eventType()); - SimpleHandler.SimpleResponse response = handler.handle(e.request(), - e.eventType(), - e.status(), - e.responseHeaders(), - e); + DirectHandler handler = ctx.directHandlers().handler(e.eventType()); + DirectHandler.TransportResponse response = handler.handle(e.request(), + e.eventType(), + e.status(), + e.responseHeaders(), + e); Optional fullResponse = e.fullResponse(); if (fullResponse.isPresent()) { @@ -290,11 +290,11 @@ public void run() { res.status(response.status()); response.headers() .forEach(res::header); - response.message().ifPresentOrElse(res::send, res::send); + response.entity().ifPresentOrElse(res::send, res::send); }); } else { HeadersServerResponse headers = response.headers(); - byte[] message = response.message().orElse(BufferData.EMPTY_BYTES); + byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES); if (message.length != 0) { headers.set(HeaderValue.create(Header.CONTENT_LENGTH, String.valueOf(message.length))); } @@ -410,7 +410,7 @@ private void handle() { throw HttpException.builder() .request(request) .response(response) - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .message(e.getMessage()) .cause(e) .build(); diff --git a/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java b/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java index 18c5faa2c06..a43aea0723d 100644 --- a/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java +++ b/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.regex.Pattern; +import io.helidon.common.http.DirectHandler; import io.helidon.config.Config; import io.helidon.config.ConfigValue; import io.helidon.nima.Nima; @@ -33,7 +34,6 @@ import io.helidon.nima.webserver.http.HttpService; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; @@ -102,7 +102,7 @@ private void value(ServerRequest req, ServerResponse res) { write(req, res, json.build()); } else { throw HttpException.builder() - .type(SimpleHandler.EventType.NOT_FOUND) + .type(DirectHandler.EventType.NOT_FOUND) .message("Config value for key: " + name) .request(req) .response(res) diff --git a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/HealthHandler.java b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/HealthHandler.java index 2c3235e76ef..37c435af6f1 100644 --- a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/HealthHandler.java +++ b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/HealthHandler.java @@ -20,12 +20,12 @@ import java.util.LinkedHashMap; import java.util.Map; +import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; import io.helidon.health.HealthCheck; import io.helidon.health.HealthCheckResponse; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; -import io.helidon.nima.webserver.HtmlEncoder; import io.helidon.nima.webserver.http.Handler; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; diff --git a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java index cf1107af58b..e781d410ea8 100644 --- a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java +++ b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java @@ -21,17 +21,17 @@ import java.util.HashMap; import java.util.Map; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; import io.helidon.health.HealthCheck; import io.helidon.health.HealthCheckResponse; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; -import io.helidon.nima.webserver.HtmlEncoder; import io.helidon.nima.webserver.http.Handler; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; import jakarta.json.JsonObject; @@ -57,7 +57,7 @@ public void handle(ServerRequest req, ServerResponse res) { throw HttpException.builder() .request(req) .response(res) - .type(SimpleHandler.EventType.NOT_FOUND) + .type(DirectHandler.EventType.NOT_FOUND) .message("Health check " + name + " does not exist") .build(); } diff --git a/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java b/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java index a60001c61a2..244bda57912 100644 --- a/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java +++ b/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java @@ -19,6 +19,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import io.helidon.common.http.DirectHandler; import io.helidon.config.Config; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; @@ -27,7 +28,6 @@ import io.helidon.nima.webserver.http.HttpService; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; @@ -62,7 +62,7 @@ private void namedInfo(ServerRequest req, ServerResponse res) { Object value = info.get(name); if (value == null) { throw HttpException.builder() - .type(SimpleHandler.EventType.NOT_FOUND) + .type(DirectHandler.EventType.NOT_FOUND) .message("Application info value for " + name + " is not defined.") .request(req) .response(res) diff --git a/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java b/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java index d3b1a3d9b1b..a6895ef16f7 100644 --- a/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java +++ b/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java @@ -22,6 +22,7 @@ import java.util.function.Consumer; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.nima.Nima; @@ -29,7 +30,6 @@ import io.helidon.nima.webserver.cors.CorsSupport; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRouting; -import io.helidon.nima.webserver.http.SimpleHandler; /** * Support for all observe providers that are available (or configured). @@ -92,7 +92,7 @@ public void accept(HttpRouting.Builder builder) { } else { builder.get(endpoint, (req, res) -> { throw HttpException.builder() - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.SERVICE_UNAVAILABLE_503) .message("Observe endpoint is disabled") .request(req) diff --git a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java index dcd5d60b82d..5053a9e64bd 100644 --- a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java +++ b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java @@ -30,7 +30,7 @@ import io.helidon.nima.webclient.ClientConnection; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; -import io.helidon.nima.webserver.http.SimpleHandlers; +import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.http1.Http1ConnectionProvider; import io.helidon.nima.webserver.spi.ServerConnection; @@ -150,7 +150,7 @@ private void startServer() { router, "unit-server", "unit-channel", - SimpleHandlers.builder().build(), + DirectHandlers.builder().build(), socket, -1); diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/BadRequestTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/BadRequestTest.java index 17fafce0ebb..1b3fb37f9e7 100644 --- a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/BadRequestTest.java +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/BadRequestTest.java @@ -18,6 +18,7 @@ import java.util.List; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersClientResponse; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; @@ -29,7 +30,6 @@ import io.helidon.nima.webclient.http1.Http1Client; import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.HttpRules; -import io.helidon.nima.webserver.http.SimpleHandler; import io.helidon.nima.webserver.http1.Http1Route; import org.junit.jupiter.api.Test; @@ -62,7 +62,7 @@ static void routing(HttpRules builder) { @SetUpServer static void setUpServer(WebServer.Builder builder) { - builder.simpleHandler(BadRequestTest::badRequestHandler, SimpleHandler.EventType.BAD_REQUEST); + builder.directHandler(BadRequestTest::badRequestHandler, DirectHandler.EventType.BAD_REQUEST); } // no need to try with resources when reading as string @@ -142,22 +142,22 @@ void testBadHeaderWhitespace() { assertThat(response, containsString(CUSTOM_ENTITY)); } - private static SimpleHandler.SimpleResponse badRequestHandler(SimpleHandler.SimpleRequest request, - SimpleHandler.EventType eventType, - Http.Status httpStatus, - HeadersServerResponse responseHeaders, - String message) { + private static DirectHandler.TransportResponse badRequestHandler(DirectHandler.TransportRequest request, + DirectHandler.EventType eventType, + Http.Status httpStatus, + HeadersServerResponse responseHeaders, + String message) { if (request.path().equals("/redirect")) { - return SimpleHandler.SimpleResponse.builder() + return DirectHandler.TransportResponse.builder() .status(Http.Status.TEMPORARY_REDIRECT_307) .header(Header.LOCATION, "/errorPage") .build(); } - return SimpleHandler.SimpleResponse.builder() + return DirectHandler.TransportResponse.builder() .status(Http.Status.create(Http.Status.BAD_REQUEST_400.code(), CUSTOM_REASON_PHRASE)) .headers(responseHeaders) - .message(CUSTOM_ENTITY) + .entity(CUSTOM_ENTITY) .build(); } } diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/XssServerTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/XssServerTest.java index f4e76b4ba62..38f7103303f 100644 --- a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/XssServerTest.java +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/XssServerTest.java @@ -18,11 +18,11 @@ import java.util.List; +import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; +import io.helidon.common.testing.http.junit5.SocketHttpClient; import io.helidon.nima.testing.junit5.webserver.ServerTest; import io.helidon.nima.testing.junit5.webserver.SetUpRoute; -import io.helidon.common.testing.http.junit5.SocketHttpClient; -import io.helidon.nima.webserver.HtmlEncoder; import io.helidon.nima.webserver.http.HttpRules; import org.junit.jupiter.api.Test; diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java index ad2b7bb0b10..649fc0c00a4 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java @@ -21,12 +21,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; record ByteRangeRequest(long fileLength, long offset, long length) { private static final Pattern RANGE_PATTERN = Pattern.compile("(\\d+)?-(\\d+)?(?:, )?"); @@ -65,7 +65,7 @@ static List parse(ServerRequest req, ServerResponse res, Strin throw HttpException.builder() .request(req) .response(res) - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .message("Invalid range header") .build(); } @@ -91,7 +91,7 @@ private static ByteRangeRequest create(ServerRequest req, ServerResponse res, lo throw HttpException.builder() .request(req) .response(res) - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .status(Http.Status.REQUESTED_RANGE_NOT_SATISFIABLE_416) .build(); } diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java index a3d074ac95b..f2902c6b4d4 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java @@ -36,11 +36,11 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; /** * Handles static content from the classpath. @@ -71,7 +71,7 @@ class ClassPathContentHandler extends FileBasedContentHandler { } catch (IOException e) { throw HttpException.builder() .message("Static content processing issue") - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(e) .build(); } @@ -83,7 +83,7 @@ class ClassPathContentHandler extends FileBasedContentHandler { } catch (IOException e) { throw HttpException.builder() .message("Static content processing issue") - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(e) .build(); } @@ -267,7 +267,7 @@ private ExtractedJarEntry extractJarEntry(URL url) { } catch (IOException ioe) { throw HttpException.builder() .message("Cannot load JAR file!") - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(ioe) .build(); } diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java index c6704896cc1..aa3a2e21346 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java @@ -32,6 +32,7 @@ import java.util.Objects; import java.util.Optional; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; @@ -43,7 +44,6 @@ import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; abstract class FileBasedContentHandler extends StaticContentHandler { private static final System.Logger LOGGER = System.getLogger(FileBasedContentHandler.class.getName()); @@ -122,7 +122,7 @@ void sendFile(Http.Method method, if (!Files.isRegularFile(path) || !Files.isReadable(path) || Files.isHidden(path)) { throw HttpException.builder() .message("File is not accessible") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.FORBIDDEN_403) .build(); } @@ -235,7 +235,7 @@ private MediaType detectType(String fileName, HeadersServerRequest requestHeader } throw HttpException.builder() .message("Media type " + it + " is not accepted by request") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.UNSUPPORTED_MEDIA_TYPE_415) .build(); }) diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java index f94fe893422..82d7dfc8db8 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.function.Function; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; @@ -35,7 +36,6 @@ import io.helidon.nima.webserver.http.PathMatchers; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.nima.webserver.http.SimpleHandler; /** * Base implementation of static content support. @@ -76,7 +76,7 @@ static void processEtag(String etag, HeadersServerRequest requestHeaders, Header if ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)) { throw HttpException.builder() .message("Accepted by If-None-Match header!") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.NOT_MODIFIED_304) .build(); } @@ -98,7 +98,7 @@ static void processEtag(String etag, HeadersServerRequest requestHeaders, Header if (!ifMatchChecked) { throw HttpException.builder() .message("Not accepted by If-Match header!") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.PRECONDITION_FAILED_412) .build(); } @@ -130,7 +130,7 @@ static void processModifyHeaders(Instant modified, if (ifModSince.isPresent() && !ifModSince.get().isBefore(modified)) { throw HttpException.builder() .message("Not valid for If-Modified-Since header!") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.NOT_MODIFIED_304) .build(); } @@ -141,7 +141,7 @@ static void processModifyHeaders(Instant modified, if (ifUnmodSince.isPresent() && ifUnmodSince.get().isBefore(modified)) { throw HttpException.builder() .message("Not valid for If-Unmodified-Since header!") - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.PRECONDITION_FAILED_412) .build(); } @@ -157,7 +157,7 @@ static void throwNotFoundIf(boolean condition) { if (condition) { throw HttpException.builder() .message("Static content not found!") - .type(SimpleHandler.EventType.NOT_FOUND) + .type(DirectHandler.EventType.NOT_FOUND) .build(); } } @@ -242,7 +242,7 @@ void handle(ServerRequest request, ServerResponse response) { LOGGER.log(Level.TRACE, "Failed to access static resource", e); throw HttpException.builder() .message("Cannot access static resource!") - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(e) .build(); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContext.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContext.java index 7a2b5287c05..8f6dc208cca 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContext.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContext.java @@ -24,7 +24,7 @@ import io.helidon.common.socket.SocketContext; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; -import io.helidon.nima.webserver.http.SimpleHandlers; +import io.helidon.nima.webserver.http.DirectHandlers; /** * Server connection context. @@ -54,7 +54,7 @@ static ConnectionContext create(MediaContext mediaContext, Router router, String serverChannelId, String channelId, - SimpleHandlers simpleHandlers, + DirectHandlers simpleHandlers, HelidonSocket socket, long maxPayloadSize) { return new ConnectionContextImpl(mediaContext, @@ -124,5 +124,5 @@ static ConnectionContext create(MediaContext mediaContext, * * @return simple handlers */ - SimpleHandlers simpleHandlers(); + DirectHandlers directHandlers(); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContextImpl.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContextImpl.java index 14df5ce8bdc..9a7de91d8ab 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContextImpl.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContextImpl.java @@ -25,7 +25,7 @@ import io.helidon.common.socket.PeerInfo; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; -import io.helidon.nima.webserver.http.SimpleHandlers; +import io.helidon.nima.webserver.http.DirectHandlers; final class ConnectionContextImpl implements ConnectionContext { private final MediaContext mediaContext; @@ -36,7 +36,7 @@ final class ConnectionContextImpl implements ConnectionContext { private final Router router; private final String socketId; private final String childSocketId; - private final SimpleHandlers simpleHandlers; + private final DirectHandlers simpleHandlers; private final HelidonSocket socket; private final long maxPayloadSize; @@ -48,7 +48,7 @@ final class ConnectionContextImpl implements ConnectionContext { Router router, String socketId, String childSocketId, - SimpleHandlers simpleHandlers, + DirectHandlers simpleHandlers, HelidonSocket socket, long maxPayloadSize) { this.mediaContext = mediaContext; @@ -100,7 +100,7 @@ public long maxPayloadSize() { } @Override - public SimpleHandlers simpleHandlers() { + public DirectHandlers directHandlers() { return simpleHandlers; } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java index c10f785a2e9..cddd1578dc2 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java @@ -27,8 +27,8 @@ import io.helidon.common.socket.SocketWriter; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; +import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.http.HttpException; -import io.helidon.nima.webserver.http.SimpleHandlers; import io.helidon.nima.webserver.spi.ServerConnection; import io.helidon.nima.webserver.spi.ServerConnectionProvider; @@ -64,7 +64,7 @@ class ConnectionHandler implements Runnable { Router router, int writeQueueLength, long maxPayloadSize, - SimpleHandlers simpleHandlers) { + DirectHandlers simpleHandlers) { this.connectionProviders = connectionProviders; this.providerCandidates = connectionProviders.providerCandidates(); this.serverChannelId = serverChannelId; diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java index c3ad2cf4cb0..d241e332219 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java @@ -32,7 +32,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; -import io.helidon.nima.webserver.http.SimpleHandlers; +import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.spi.ServerConnectionProvider; class LoomServer implements WebServer { @@ -48,7 +48,7 @@ class LoomServer implements WebServer { private volatile List startFutures; private boolean alreadyStarted = false; - LoomServer(Builder builder, SimpleHandlers simpleHandlers) { + LoomServer(Builder builder, DirectHandlers simpleHandlers) { List connectionProviders = builder.connectionProviders(); Map routers = builder.routers(); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java index ffb415504b1..f4e8c4933d8 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java @@ -41,7 +41,7 @@ import io.helidon.nima.common.tls.Tls; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; -import io.helidon.nima.webserver.http.SimpleHandlers; +import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.spi.ServerConnectionProvider; import static java.lang.System.Logger.Level.INFO; @@ -57,7 +57,7 @@ class ServerListener { private final ExecutorService readerExecutor; private final ExecutorService sharedExecutor; private final Thread serverThread; - private final SimpleHandlers simpleHandlers; + private final DirectHandlers simpleHandlers; private final CompletableFuture closeFuture; private final SocketOptions connectionOptions; private final InetSocketAddress configuredAddress; @@ -76,7 +76,7 @@ class ServerListener { String socketName, ListenerConfiguration listenerConfig, Router router, - SimpleHandlers simpleHandlers) { + DirectHandlers simpleHandlers) { this.server = loomServer; this.connectionProviders = ConnectionProviders.create(connectionProviders); this.socketName = socketName; diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java index 648b4c6687b..bdfae1793b6 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java @@ -26,11 +26,11 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LogConfig; +import io.helidon.common.http.DirectHandler; import io.helidon.config.Config; import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.http.HttpRouting; -import io.helidon.nima.webserver.http.SimpleHandler; -import io.helidon.nima.webserver.http.SimpleHandlers; import io.helidon.nima.webserver.spi.ServerConnectionProvider; /** @@ -127,7 +127,7 @@ class Builder implements io.helidon.common.Builder, Router.R private final Map socketBuilder = new HashMap<>(); private final Map routers = new HashMap<>(); - private final SimpleHandlers.Builder simpleHandlers = SimpleHandlers.builder(); + private final DirectHandlers.Builder simpleHandlers = DirectHandlers.builder(); private final HelidonServiceLoader.Builder connectionProviders = HelidonServiceLoader.builder(ServiceLoader.load(ServerConnectionProvider.class)); @@ -277,8 +277,8 @@ public Builder host(String host) { * @param eventTypes event types this handler should handle * @return updated builder */ - public Builder simpleHandler(SimpleHandler handler, SimpleHandler.EventType... eventTypes) { - for (SimpleHandler.EventType eventType : eventTypes) { + public Builder directHandler(DirectHandler handler, DirectHandler.EventType... eventTypes) { + for (DirectHandler.EventType eventType : eventTypes) { simpleHandlers.addHandler(eventType, handler); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandlers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java similarity index 66% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandlers.java rename to nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java index 48ab0a292fc..31f74793869 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/SimpleHandlers.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java @@ -19,23 +19,22 @@ import java.util.EnumMap; import java.util.Map; -import io.helidon.common.http.HeadersServerResponse; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.DirectHandler.EventType; import io.helidon.common.http.Http; import io.helidon.nima.webserver.CloseConnectionException; -import io.helidon.nima.webserver.HtmlEncoder; -import io.helidon.nima.webserver.http.SimpleHandler.EventType; import static java.lang.System.Logger.Level.WARNING; /** * Configured handlers for expected (and internal) exceptions. */ -public class SimpleHandlers { - private static final System.Logger LOGGER = System.getLogger(SimpleHandlers.class.getName()); +public class DirectHandlers { + private static final System.Logger LOGGER = System.getLogger(DirectHandlers.class.getName()); - private final Map handlers; + private final Map handlers; - private SimpleHandlers(Map handlers) { + private DirectHandlers(Map handlers) { this.handlers = new EnumMap<>(handlers); } @@ -55,7 +54,7 @@ public static Builder builder() { * @param eventType event type * @return handler to use */ - public SimpleHandler handler(EventType eventType) { + public DirectHandler handler(EventType eventType) { return handlers.get(eventType); } @@ -66,7 +65,7 @@ public SimpleHandler handler(EventType eventType) { * @param res response */ public void handle(HttpException httpException, ServerResponse res) { - SimpleHandler.SimpleResponse response = handler(httpException.eventType()).handle( + DirectHandler.TransportResponse response = handler(httpException.eventType()).handle( httpException.request(), httpException.eventType(), httpException.status(), @@ -88,11 +87,11 @@ public void handle(HttpException httpException, ServerResponse res) { } try { - response.message().ifPresentOrElse(res::send, res::send); + response.entity().ifPresentOrElse(res::send, res::send); } catch (IllegalStateException ex) { // failed to send - probably output stream was already obtained and used, so status is written // we can only close the connection now - res.streamResult(response.message().map(String::new).orElseGet(() -> httpException.getCause().getMessage())); + res.streamResult(response.entity().map(String::new).orElseGet(() -> httpException.getCause().getMessage())); throw new CloseConnectionException( "Cannot send response of a simple handler, status and headers already written", ex); @@ -106,21 +105,21 @@ public void handle(HttpException httpException, ServerResponse res) { } /** - * Fluent API builder for {@link SimpleHandlers}. + * Fluent API builder for {@link DirectHandlers}. */ - public static class Builder implements io.helidon.common.Builder { - private final Map handlers = new EnumMap<>(EventType.class); - private final SimpleHandler defaultHandler = new DefaultHandler(); + public static class Builder implements io.helidon.common.Builder { + private final Map handlers = new EnumMap<>(EventType.class); + private final DirectHandler defaultHandler = DirectHandler.defaultHandler(); private Builder() { } @Override - public SimpleHandlers build() { + public DirectHandlers build() { for (EventType value : EventType.values()) { handlers.putIfAbsent(value, defaultHandler); } - return new SimpleHandlers(handlers); + return new DirectHandlers(handlers); } /** @@ -130,28 +129,9 @@ public SimpleHandlers build() { * @param handler handler to handle that type * @return updated builder */ - public Builder addHandler(EventType eventType, SimpleHandler handler) { + public Builder addHandler(EventType eventType, DirectHandler handler) { handlers.put(eventType, handler); return this; } } - - private static class DefaultHandler implements SimpleHandler { - @Override - public SimpleResponse handle(SimpleRequest request, - EventType eventType, - Http.Status defaultStatus, - HeadersServerResponse headers, - String message) { - return SimpleResponse.builder() - .status(defaultStatus) - .headers(headers) - .update(it -> { - if (!message.isEmpty()) { - it.message(HtmlEncoder.encode(message)); - } - }) - .build(); - } - } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java index 073a610c208..27200a1065d 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java @@ -19,6 +19,7 @@ import java.util.Iterator; import java.util.List; +import io.helidon.common.http.DirectHandler; import io.helidon.common.parameters.Parameters; import io.helidon.common.uri.UriPath; @@ -92,7 +93,7 @@ public void proceed() { throw HttpException.builder() .request(request) .response(response) - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .message("Routing finished but response was not sent. NĂ­ma does not support asynchronous responses. " + "Please block until a response is sent.") .build(); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java index de38b1bf9f9..a27e990dd54 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java @@ -18,6 +18,7 @@ import java.util.Optional; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; @@ -25,18 +26,24 @@ * HTTP exception. This allows custom handlers to be used for different even types. */ public class HttpException extends RuntimeException { - private final SimpleHandler.EventType eventType; + private final DirectHandler.EventType eventType; private final Http.Status status; - private final SimpleHandler.SimpleRequest simpleRequest; + private final DirectHandler.TransportRequest transportRequest; private final ServerResponse fullResponse; private final boolean keepAlive; private final HeadersServerResponse responseHeaders; - private HttpException(Builder builder) { + /** + * A new exception with a predefined status, even type. + * Additional information may be added to simplify handling. + * + * @param builder builder with details to create this instance + */ + protected HttpException(Builder builder) { super(builder.message, builder.cause); this.eventType = builder.type; this.status = builder.status; - this.simpleRequest = builder.request; + this.transportRequest = builder.request; this.fullResponse = builder.fullResponse; this.keepAlive = builder.keepAlive; this.responseHeaders = builder.responseHeaders; @@ -65,17 +72,17 @@ public Http.Status status() { * * @return event type */ - public SimpleHandler.EventType eventType() { + public DirectHandler.EventType eventType() { return eventType; } /** - * Simple request with as much information as is available. + * Transport request with as much information as is available. * * @return request */ - public SimpleHandler.SimpleRequest request() { - return simpleRequest; + public DirectHandler.TransportRequest request() { + return transportRequest; } /** @@ -112,12 +119,12 @@ public HeadersServerResponse responseHeaders() { public static class Builder implements io.helidon.common.Builder { private String message; private Throwable cause; - private SimpleHandler.SimpleRequest request; - private SimpleHandler.EventType type; + private DirectHandler.TransportRequest request; + private DirectHandler.EventType type; private Http.Status status; private ServerResponse fullResponse; private Boolean keepAlive; - private HeadersServerResponse responseHeaders = HeadersServerResponse.create(); + private final HeadersServerResponse responseHeaders = HeadersServerResponse.create(); private Builder() { } @@ -128,10 +135,10 @@ public HttpException build() { message = ""; } if (request == null) { - request = SimpleHandler.SimpleRequest.empty(); + request = DirectHandler.TransportRequest.empty(); } if (type == null) { - type(SimpleHandler.EventType.INTERNAL_ERROR); + type(DirectHandler.EventType.INTERNAL_ERROR); } return new HttpException(this); } @@ -170,7 +177,7 @@ public Builder request(ServerRequest request) { } /** - * Routing response to be used to handle response from simple handler. + * Routing response to be used to handle response from direct handler. * * @param response response to use * @return updated builder @@ -181,12 +188,12 @@ public Builder response(ServerResponse response) { } /** - * Simple request with as much information as is available. + * Transport request with as much information as is available. * * @param request request to use * @return updated builder */ - public Builder request(SimpleHandler.SimpleRequest request) { + public Builder request(DirectHandler.TransportRequest request) { this.request = request; return this; } @@ -197,7 +204,7 @@ public Builder request(SimpleHandler.SimpleRequest request) { * @param type type to use * @return updated builder */ - public Builder type(SimpleHandler.EventType type) { + public Builder type(DirectHandler.EventType type) { this.type = type; if (status == null) { status = type.defaultStatus(); @@ -210,7 +217,7 @@ public Builder type(SimpleHandler.EventType type) { /** * Http status to use. This will override default status from - * {@link SimpleHandler.EventType#defaultStatus()}. + * {@link io.helidon.common.http.DirectHandler.EventType#defaultStatus()}. * * @param status status to use * @return updated builder diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java index 8076e41ab79..69a2a8f0dbc 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java @@ -25,6 +25,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.common.http.HttpPrologue; import io.helidon.nima.webserver.CloseConnectionException; @@ -306,7 +307,7 @@ public void run() { throw HttpException.builder() .request(HttpSimpleRequest.create(prologue, request.headers())) - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .build(); } @@ -317,14 +318,14 @@ public void run() { if (result == RoutingResult.FINISH) { return; } - ctx.simpleHandlers().handle(HttpException.builder() + ctx.directHandlers().handle(HttpException.builder() .request(request) .response(response) - .type(SimpleHandler.EventType.NOT_FOUND) + .type(DirectHandler.EventType.NOT_FOUND) .message("Endpoint not found") .build(), response); } catch (HttpException e) { - ctx.simpleHandlers().handle(e, response); + ctx.directHandlers().handle(e, response); } } @@ -391,7 +392,7 @@ private RoutingResult doRoute(ConnectionContext ctx, RoutingRequest request, Rou ctx.log(LOGGER, System.Logger.Level.WARNING, "Request failed", thrown); // we must close connection, as we could not consume request throw HttpException.builder() - .type(SimpleHandler.EventType.INTERNAL_ERROR) + .type(DirectHandler.EventType.INTERNAL_ERROR) .cause(thrown) .request(request) .response(response) diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java index 3c7c9f44b9b..3bb70391fa6 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java @@ -16,26 +16,25 @@ package io.helidon.nima.webserver.http; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Headers; +import io.helidon.common.http.HeadersServerRequest; +import io.helidon.common.http.HeadersWritable; import io.helidon.common.http.HttpPrologue; /** * Simple request to use with {@link HttpException}. */ -public class HttpSimpleRequest implements SimpleHandler.SimpleRequest { +public class HttpSimpleRequest implements DirectHandler.TransportRequest { private final String version; private final String method; private final String path; - private final Map> headers; + private final HeadersServerRequest headers; private HttpSimpleRequest(String version, String method, String path, - Map> headers) { + HeadersServerRequest headers) { this.version = version; this.method = method; this.path = path; @@ -48,14 +47,15 @@ private HttpSimpleRequest(String version, * @param protocolAndVersion protocol with version * @param method method * @param path path - * @param headers headers * @return a new simple request */ - public static SimpleHandler.SimpleRequest create(String protocolAndVersion, - String method, - String path, - Map> headers) { - return new HttpSimpleRequest(protocolAndVersion, method, path, headers); + public static DirectHandler.TransportRequest create(String protocolAndVersion, + String method, + String path) { + return new HttpSimpleRequest(protocolAndVersion, + method, + path, + HeadersServerRequest.create(HeadersWritable.create())); } /** @@ -65,14 +65,11 @@ public static SimpleHandler.SimpleRequest create(String protocolAndVersion, * @param headers parsed headers * @return a new simple request */ - public static SimpleHandler.SimpleRequest create(HttpPrologue prologue, Headers headers) { - Map> headerMap = new HashMap<>(); - headers.forEach(it -> headerMap.put(it.name(), it.allValues())); - + public static DirectHandler.TransportRequest create(HttpPrologue prologue, Headers headers) { return new HttpSimpleRequest(prologue.protocol() + "/" + prologue.protocolVersion(), prologue.method().text(), prologue.uriPath().rawPathNoParams(), - headerMap); + HeadersServerRequest.create(headers)); } @Override @@ -91,7 +88,7 @@ public String path() { } @Override - public Map> headers() { + public HeadersServerRequest headers() { return headers; } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java index 046157a1e3f..b21acf888bf 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java @@ -25,6 +25,7 @@ import io.helidon.common.GenericType; import io.helidon.common.buffers.BufferData; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.Http; import io.helidon.common.http.HttpPrologue; @@ -107,7 +108,7 @@ public void send(Object entity) { } catch (IllegalArgumentException e) { throw HttpException.builder() .message(e.getMessage()) - .type(SimpleHandler.EventType.OTHER) + .type(DirectHandler.EventType.OTHER) .status(Http.Status.UNSUPPORTED_MEDIA_TYPE_415) .request(HttpSimpleRequest.create(requestPrologue, requestHeaders)) .build(); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java index f54d47a1a7e..e0b4e1409a3 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java @@ -23,6 +23,8 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.DirectHandler.EventType; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.HeadersWritable; @@ -37,8 +39,6 @@ import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRouting; import io.helidon.nima.webserver.http.HttpSimpleRequest; -import io.helidon.nima.webserver.http.SimpleHandler; -import io.helidon.nima.webserver.http.SimpleHandler.EventType; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; import io.helidon.nima.webserver.spi.ServerConnection; @@ -337,23 +337,23 @@ private void consumeEntity(Http1ServerRequest request, Http1ServerResponse respo private void handleHttpException(HttpException e) { if (e.fullResponse().isPresent()) { - ctx.simpleHandlers().handle(e, e.fullResponse().get()); + ctx.directHandlers().handle(e, e.fullResponse().get()); return; } - SimpleHandler handler = ctx.simpleHandlers().handler(e.eventType()); - SimpleHandler.SimpleResponse response = handler.handle(e.request(), - e.eventType(), - e.status(), - e.responseHeaders(), - e); + DirectHandler handler = ctx.directHandlers().handler(e.eventType()); + DirectHandler.TransportResponse response = handler.handle(e.request(), + e.eventType(), + e.status(), + e.responseHeaders(), + e); BufferData buffer = BufferData.growing(128); HeadersServerResponse headers = response.headers(); if (!e.keepAlive()) { headers.set(HeaderValues.CONNECTION_CLOSE); } - byte[] message = response.message().orElse(BufferData.EMPTY_BYTES); + byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES); if (message.length != 0) { headers.set(Http.HeaderValue.create(Http.Header.CONTENT_LENGTH, String.valueOf(message.length))); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java index 1c79b28aa07..0746cd43894 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java @@ -17,12 +17,12 @@ package io.helidon.nima.webserver.http1; import io.helidon.common.buffers.DataReader; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersWritable; import io.helidon.common.http.Http1HeadersParser; import io.helidon.common.http.HttpPrologue; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpSimpleRequest; -import io.helidon.nima.webserver.http.SimpleHandler; /** * HTTP/1 headers reader. @@ -57,7 +57,7 @@ public HeadersWritable readHeaders(HttpPrologue prologue) { return Http1HeadersParser.readHeaders(reader, maxHeadersSize, validateHeaders); } catch (IllegalStateException | IllegalArgumentException e) { throw HttpException.builder() - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .request(HttpSimpleRequest.create(prologue, HeadersWritable.create())) .message(e.getMessage()) .cause(e) diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java index 22c2ecbbdaf..2fb96163191 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java @@ -17,16 +17,15 @@ package io.helidon.nima.webserver.http1; import java.nio.charset.StandardCharsets; -import java.util.Map; import io.helidon.common.buffers.Bytes; import io.helidon.common.buffers.DataReader; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.common.http.HttpPrologue; import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpSimpleRequest; -import io.helidon.nima.webserver.http.SimpleHandler; /** * HTTP 1 prologue parsing support. @@ -72,8 +71,8 @@ private static HttpException badRequest(String message, String method, String pa protocolAndVersion = protocol + "/" + version; } return HttpException.builder() - .type(SimpleHandler.EventType.BAD_REQUEST) - .request(HttpSimpleRequest.create(protocolAndVersion, method, path, Map.of())) + .type(DirectHandler.EventType.BAD_REQUEST) + .request(HttpSimpleRequest.create(protocolAndVersion, method, path)) .message(message) .build(); } @@ -116,9 +115,9 @@ private HttpPrologue doRead() { } else if (secondSpace == maxLen) { throw HttpException.builder() .message("Request URI too long.") - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .status(Http.Status.REQUEST_URI_TOO_LONG_414) - .request(HttpSimpleRequest.create("", method.text(), reader.readAsciiString(secondSpace), Map.of())) + .request(HttpSimpleRequest.create("", method.text(), reader.readAsciiString(secondSpace))) .build(); } path = reader.readAsciiString(secondSpace); @@ -135,7 +134,7 @@ private HttpPrologue doRead() { } catch (DataReader.IncorrectNewLineException e) { throw HttpException.builder() .message("Invalid prologue: " + e.getMessage()) - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .cause(e) .build(); } diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java index 17874893360..79c904257cf 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java @@ -26,6 +26,7 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataWriter; +import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersWritable; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; @@ -33,7 +34,6 @@ import io.helidon.common.http.HttpPrologue; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.http.HttpException; -import io.helidon.nima.webserver.http.SimpleHandler; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; import io.helidon.nima.webserver.spi.ServerConnection; @@ -106,7 +106,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, He if (!SUPPORTED_VERSION.equals(version)) { throw HttpException.builder() - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .message("Unsupported WebSocket Version") .header(SUPPORTED_VERSION_HEADER) .build(); @@ -125,7 +125,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, He if (!origins.contains(origin)) { throw HttpException.builder() .message("Invalid Origin") - .type(SimpleHandler.EventType.FORBIDDEN) + .type(DirectHandler.EventType.FORBIDDEN) .build(); } } @@ -156,7 +156,7 @@ private String hash(ConnectionContext ctx, String wsKey) { MUST be selected randomly for each connection. */ throw HttpException.builder() - .type(SimpleHandler.EventType.BAD_REQUEST) + .type(DirectHandler.EventType.BAD_REQUEST) .message("Invalid Sec-WebSocket-Key header") .build(); } diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandler.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandler.java deleted file mode 100644 index 45a59082506..00000000000 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandler.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * 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.helidon.reactive.webserver; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; - -/** - * A handler that is invoked when a response is sent outside of routing. - * See {@link DirectHandler.EventType} to see which types - * of events are covered by this handler. - */ -@FunctionalInterface -public interface DirectHandler { - /** - * Handler of responses that bypass routing, MUST NOT block the current thread. - *

- * This method should be used to return custom status, header and possible entity (retrieved without blocking). - * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. - * - * @param request request as received with as much known information as possible - * @param eventType type of the event - * @param defaultStatus default status expected to be returned - * @param t throwable caught as part of processing with possible additional details about the reason of failure - * @return response to use to return to original request - */ - default TransportResponse handle(TransportRequest request, - EventType eventType, - Http.Status defaultStatus, - Throwable t) { - return handle(request, eventType, defaultStatus, t.getMessage()); - } - - /** - * Handler of responses that bypass routing, MUST NOT block the current thread. - *

- * This method should be used to return custom status, header and possible entity (retrieved without blocking). - * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. - * - * @param request request as received with as much known information as possible - * @param eventType type of the event - * @param defaultStatus default status expected to be returned - * @param message message expected to be used as a response entity - may be an empty string (no entity expected), never null - * @return response to use to return to original request - */ - TransportResponse handle(TransportRequest request, - EventType eventType, - Http.Status defaultStatus, - String message); - - /** - * Request information. - * Note that the information may not be according to specification, as this marks a bad request (by definition). - */ - interface TransportRequest { - /** - * Protocol version (either from actual request, or guessed). - * - * @return protocol version - */ - String protocolVersion(); - - /** - * HTTP method. - * - * @return method - */ - String method(); - - /** - * Requested URI, if found in request. - * - * @return uri or an empty string - */ - String uri(); - - /** - * Headers, if found in request. - * - * @return headers or an empty map - */ - Map> headers(); - } - - /** - * Types of events that can be triggered outside of routing - * that immediately return a response. - */ - enum EventType { - /** - * Bad request, such as invalid path, header. - */ - BAD_REQUEST, - /** - * Payload is bigger than the configured maximal size. - */ - PAYLOAD_TOO_LARGE, - /** - * Continue (see {@link Http.Status#CONTINUE_100}). - */ - CONTINUE - } - - /** - * Response to correctly reply to the original client. - */ - class TransportResponse { - private final Http.Status status; - private final Map> headers; - private final byte[] entity; - - private TransportResponse(Builder builder) { - this.status = builder.status; - this.headers = builder.headers; - this.entity = builder.entity; - } - - /** - * A builder to set up a custom response. - * - * @return builder - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Create a response with {@link Http.Status#BAD_REQUEST_400} status and provided message. - * - * @param message message to send as response entity - * @return a new response - */ - public static TransportResponse create(String message) { - return builder().entity(message).build(); - } - - Http.Status status() { - return status; - } - - Map> headers() { - return headers; - } - - Optional entity() { - return Optional.ofNullable(entity); - } - - /** - * Fluent API builder for {@link DirectHandler.TransportResponse}. - */ - public static class Builder implements io.helidon.common.Builder { - private final Map> headers = new HashMap<>(); - - private Http.Status status = Http.Status.BAD_REQUEST_400; - private byte[] entity; - - private Builder() { - } - - @Override - public TransportResponse build() { - return new TransportResponse(this); - } - - /** - * Custom status. - * - * @param status status to use, default is bad request - * @return updated builder - */ - public Builder status(Http.Status status) { - this.status = status; - return this; - } - - /** - * Add/replace a header. - * - * @param name name of the header - * @param values value of the header - * @return updated builder - * @throws java.lang.IllegalArgumentException if an attempt is made to modify protected headers (such as Connection) - */ - public Builder header(String name, String... values) { - if (name.equalsIgnoreCase(Http.Header.CONNECTION.lowerCase())) { - throw new IllegalArgumentException( - "Connection header cannot be overridden, it is always set to Close fro transport errors"); - } - this.headers.put(name, List.of(values)); - return this; - } - - /** - * Custom entity. Uses the content, encodes it for HTML, reads it as {@code UTF-8}, configures - * {@code Content-Length} header, configures {@code Content-Type} header to {@code text/plain}. - *

- * Use {@link #entity(byte[])} for custom encoding. - * - * @param entity response entity - * @return updated builder - */ - public Builder entity(String entity) { - this.headers.putIfAbsent(Http.Header.CONTENT_TYPE.defaultCase(), List.of(HttpMediaType.PLAINTEXT_UTF_8.text())); - return entity(HtmlEncoder.encode(entity).getBytes(StandardCharsets.UTF_8)); - } - - /** - * Custom entity. Uses the content, configures - * {@code Content-Length} header. - *

- * Use {@link #entity(String)} for simple text messages. - * - * @param entity response entity - * @return updated builder - */ - public Builder entity(byte[] entity) { - this.entity = Arrays.copyOf(entity, entity.length); - if (this.entity.length == 0) { - this.headers.remove(Http.Header.CONTENT_LENGTH.defaultCase()); - } else { - this.header(Http.Header.CONTENT_LENGTH.defaultCase(), String.valueOf(entity.length)); - } - return this; - } - } - } -} diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandlers.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandlers.java index eb420e4d975..999ff7bd263 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandlers.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/DirectHandlers.java @@ -19,7 +19,7 @@ import java.util.EnumMap; import java.util.Map; -import io.helidon.common.http.Http; +import io.helidon.common.http.DirectHandler; class DirectHandlers { private final Map handlers; @@ -38,7 +38,7 @@ DirectHandler handler(DirectHandler.EventType eventType) { static class Builder implements io.helidon.common.Builder { private final Map handlers = new EnumMap<>(DirectHandler.EventType.class); - private final DirectHandler defaultHandler = new DefaultHandler(); + private final DirectHandler defaultHandler = DirectHandler.defaultHandler(); private Builder() { } @@ -56,22 +56,4 @@ Builder addHandler(DirectHandler.EventType eventType, DirectHandler handler) { return this; } } - - private static class DefaultHandler implements DirectHandler { - @Override - public TransportResponse handle(TransportRequest request, - EventType eventType, - Http.Status defaultStatus, - String message) { - - TransportResponse.Builder builder = TransportResponse.builder() - .status(defaultStatus); - - if (!message.isEmpty()) { - builder.entity(message); - } - - return builder.build(); - } - } } diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java index fa8b408e2a6..0eadab6cf2d 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java @@ -21,9 +21,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; @@ -34,10 +32,14 @@ import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.DirectHandler.TransportResponse; +import io.helidon.common.http.HeadersServerRequest; +import io.helidon.common.http.HeadersServerResponse; +import io.helidon.common.http.HeadersWritable; import io.helidon.common.http.Http; import io.helidon.logging.common.HelidonMdc; import io.helidon.reactive.webserver.ByteBufRequestChunk.DataChunkHoldingQueue; -import io.helidon.reactive.webserver.DirectHandler.TransportResponse; import io.helidon.reactive.webserver.ReferenceHoldingQueue.IndirectReference; import io.netty.buffer.ByteBuf; @@ -433,7 +435,7 @@ TODO we should only send continue in case the entity is request (e.g. we found a This would solve connection close for 404 for requests with entity */ if (HttpUtil.is100ContinueExpected(request)) { - send100Continue(ctx, request); + send100Continue(ctx); } // If a problem during routing, return 400 response @@ -484,16 +486,11 @@ private void checkDecoderResult(HttpRequest request) { } } - private void send100Continue(ChannelHandlerContext ctx, - HttpRequest request) { + private void send100Continue(ChannelHandlerContext ctx) { - TransportResponse transportResponse = directHandlers.handler(DirectHandler.EventType.CONTINUE) - .handle(new DirectHandlerRequest(request), - DirectHandler.EventType.CONTINUE, - Http.Status.CONTINUE_100, - ""); + // continue is not a full HTTP response + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.CONTINUE); - FullHttpResponse response = toNettyResponse(transportResponse); // we should flush this immediately, as we need the client to send entity ctx.writeAndFlush(response); } @@ -511,6 +508,7 @@ private void send400BadRequest(ChannelHandlerContext ctx, HttpRequest request, T .handle(new DirectHandlerRequest(request), DirectHandler.EventType.BAD_REQUEST, Http.Status.BAD_REQUEST_400, + HeadersServerResponse.create(), t); FullHttpResponse response = toNettyResponse(handlerResponse); @@ -533,6 +531,7 @@ private void send413PayloadTooLarge(ChannelHandlerContext ctx, HttpRequest reque .handle(new DirectHandlerRequest(request), DirectHandler.EventType.PAYLOAD_TOO_LARGE, Http.Status.REQUEST_ENTITY_TOO_LARGE_413, + HeadersServerResponse.create(), ""); FullHttpResponse response = toNettyResponse(transportResponse); @@ -548,7 +547,7 @@ private void send413PayloadTooLarge(ChannelHandlerContext ctx, HttpRequest reque private FullHttpResponse toNettyResponse(TransportResponse handlerResponse) { Optional entity = handlerResponse.entity(); Http.Status status = handlerResponse.status(); - Map> headers = handlerResponse.headers(); + HeadersServerResponse headers = handlerResponse.headers(); HttpResponseStatus nettyStatus = HttpResponseStatus.valueOf(status.code(), status.reasonPhrase()); @@ -558,7 +557,7 @@ private FullHttpResponse toNettyResponse(TransportResponse handlerResponse) { .orElseGet(() -> new DefaultFullHttpResponse(HTTP_1_1, nettyStatus)); HttpHeaders nettyHeaders = response.headers(); - headers.forEach(nettyHeaders::add); + headers.forEach(it -> nettyHeaders.add(it.name(), it.allValues())); return response; } @@ -595,17 +594,17 @@ private static final class DirectHandlerRequest implements DirectHandler.Transpo private final String protocolVersion; private final String uri; private final String method; - private final Map> headers; + private final HeadersServerRequest headers; private DirectHandlerRequest(HttpRequest request) { protocolVersion = request.protocolVersion().text(); uri = request.uri(); method = request.method().name(); - Map> result = new HashMap<>(); + HeadersWritable result = HeadersWritable.create(); for (String name : request.headers().names()) { - result.put(name, request.headers().getAll(name)); + result.add(Http.HeaderValue.create(Http.Header.create(name), request.headers().getAll(name))); } - headers = Map.copyOf(result); + headers = HeadersServerRequest.create(result); } @Override @@ -614,7 +613,7 @@ public String protocolVersion() { } @Override - public String uri() { + public String path() { return uri; } @@ -624,7 +623,7 @@ public String method() { } @Override - public Map> headers() { + public HeadersServerRequest headers() { return headers; } } diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HtmlEncoder.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HtmlEncoder.java deleted file mode 100644 index 8ac4d93ab85..00000000000 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HtmlEncoder.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * 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.helidon.reactive.webserver; - -/** - * HTML encoding of special characters to prevent cross site scripting (XSS) attacks. - * Any data that is "echoed" back from a request can be used to execute a script in a - * browser unless properly encoded. - */ -public final class HtmlEncoder { - - private HtmlEncoder() { - } - - /** - * Encode HTML string replacing the special characters by their corresponding - * entities. - * - * @param s string to encode. - * @return encoded string. - */ - public static String encode(String s) { - int n = s.length(); - StringBuilder result = new StringBuilder(n); - for (int i = 0; i < n; i++) { - char c = s.charAt(i); - switch (c) { - case '&': - result.append("&"); - break; - case '<': - result.append("<"); - break; - case '>': - result.append(">"); - break; - case '"': - result.append("""); - break; - case '\'': - result.append("'"); - break; - default: - result.append(c); - break; - } - } - return result.toString(); - } -} diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java index 438ced59ecf..957cff71ee7 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java @@ -33,6 +33,7 @@ import io.helidon.common.LazyValue; import io.helidon.common.context.Contexts; +import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; import io.helidon.common.http.HttpMediaType; import io.helidon.tracing.Span; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/WebServer.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/WebServer.java index 2878383ce16..c0638522084 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/WebServer.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/WebServer.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import io.helidon.common.context.Context; +import io.helidon.common.http.DirectHandler; import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.config.metadata.Configured; @@ -776,7 +777,7 @@ public Builder printFeatureDetails(boolean shouldPrint) { * Provide a custom handler for events that bypass routing. * The handler can customize status, headers and message. *

- * Examples of bad request ({@link DirectHandler.EventType#BAD_REQUEST}: + * Examples of bad request ({@link io.helidon.common.http.DirectHandler.EventType#BAD_REQUEST}: *