From 8e210c4f3cca7be46f757ae16e75622e7708ef71 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 29 Jan 2025 14:06:55 -0500 Subject: [PATCH] Gracefully reject HTTP 1.0 connections that are either ignored by all our providers or directly routed to our 1.1 provider. We always route to 1.1 if only a single provider is found. Returns HTTP error code 505 (HTTP version not supported) instead of 400 as before. --- .../java/io/helidon/http/DirectHandler.java | 22 ++++++------ .../helidon/webserver/ConnectionHandler.java | 19 ++++++++++- .../webserver/http1/Http1Prologue.java | 25 +++++++++++++- .../webserver/ConnectionHandlerTest.java | 34 +++++++++++++++++++ .../webserver/http1/Http1PrologueTest.java | 19 ++++++++++- 5 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 webserver/webserver/src/test/java/io/helidon/webserver/ConnectionHandlerTest.java diff --git a/http/http/src/main/java/io/helidon/http/DirectHandler.java b/http/http/src/main/java/io/helidon/http/DirectHandler.java index 5ad6aaba1ef..8828658721e 100644 --- a/http/http/src/main/java/io/helidon/http/DirectHandler.java +++ b/http/http/src/main/java/io/helidon/http/DirectHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2025 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. @@ -85,15 +85,11 @@ default TransportResponse handle(TransportRequest request, Throwable thrown, System.Logger logger) { if (thrown instanceof RequestException re) { - if (re.safeMessage()) { - return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage()); - } else { - if (logger != null) { - logger.log(Level.DEBUG, thrown); - } - return handle(request, eventType, defaultStatus, responseHeaders, - "Bad request, see server log for more information"); + if (logger != null) { + logger.log(Level.DEBUG, thrown); } + return handle(request, eventType, defaultStatus, responseHeaders, + re.safeMessage() ? thrown.getMessage() : "Bad request, see server log for more information"); } return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage()); } @@ -188,7 +184,11 @@ enum EventType { /** * Other type, please specify expected status code. */ - OTHER(Status.INTERNAL_SERVER_ERROR_500, true); + OTHER(Status.INTERNAL_SERVER_ERROR_500, true), + /** + * HTTP version not supported. + */ + HTTP_VERSION_NOT_SUPPORTED(Status.HTTP_VERSION_NOT_SUPPORTED_505, false); private final Status defaultStatus; private final boolean keepAlive; @@ -210,7 +210,7 @@ public Status defaultStatus() { /** * Whether keep alive should be maintained for this event type. * - * @return whether to keep connectino alive + * @return whether to keep connection alive */ public boolean keepAlive() { return keepAlive; diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index 4dc9106fe19..2aaa02979cf 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -30,6 +30,7 @@ import javax.net.ssl.SSLSocket; import io.helidon.common.buffers.BufferData; +import io.helidon.common.buffers.Bytes; import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; import io.helidon.common.concurrency.limits.Limit; @@ -55,6 +56,7 @@ */ class ConnectionHandler implements InterruptableTask, ConnectionContext { private static final System.Logger LOGGER = System.getLogger(ConnectionHandler.class.getName()); + private static final String HTTP_1_0 = "HTTP/1.0\r"; private final ListenerContext listenerContext; // we must safely release the semaphore whenever this connection is finished, so other connections can be created! @@ -163,6 +165,10 @@ public final void run() { } if (connection == null) { + if (isHttp10Connection(reader)) { + // cannot easily return 505, so log better message instead + throw new CloseConnectionException("HTTP 1.0 is not supported, consider using HTTP 1.1"); + } throw new CloseConnectionException("No suitable connection provider"); } activeConnections.put(socketsId, connection); @@ -333,6 +339,17 @@ private void closeChannel() { } } + static boolean isHttp10Connection(DataReader reader) { + try { + reader.ensureAvailable(); + } catch (DataReader.InsufficientDataAvailableException e) { + throw new CloseConnectionException("No data available", e); + } + BufferData request = reader.getBuffer(reader.available()); + int lf = request.indexOf(Bytes.LF_BYTE); + return lf != -1 && request.readString(lf).endsWith(HTTP_1_0); + } + private static class MapExceptionDataSupplier implements Supplier { private final HelidonSocket helidonSocket; diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Prologue.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Prologue.java index 5581e49e81b..aad61fbba09 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Prologue.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Prologue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -44,6 +44,17 @@ public final class Http1Prologue { | (long) '.' << 48 | (long) '1' << 56; /* + The string HTTP/1.0: we don't support 1.0, but we detect it + */ + private static final long HTTP_1_0_LONG = 'H' + | 'T' << 8 + | 'T' << 16 + | 'P' << 24 + | (long) '/' << 32 + | (long) '1' << 40 + | (long) '.' << 48 + | (long) '0' << 56; + /* GET string as int */ private static final int GET_INT = 'G' @@ -63,6 +74,7 @@ public final class Http1Prologue { | 'S' << 16 | 'T' << 24; private static final String HTTP_1_1 = "HTTP/1.1"; + private static final String HTTP_1_0 = "HTTP/1.0"; private final DataReader reader; private final int maxLength; @@ -206,6 +218,15 @@ Read HTTP Version (we only support HTTP/1.1 // we always use the same constant //noinspection StringEquality if (protocol != HTTP_1_1) { + //noinspection StringEquality + if (protocol == HTTP_1_0) { // be friendly rejecting 1.0 + throw RequestException.builder() + .type(DirectHandler.EventType.HTTP_VERSION_NOT_SUPPORTED) + .request(DirectTransportRequest.create(HTTP_1_0, method.text(), path)) + .message("HTTP 1.0 is not supported, consider using HTTP 1.1") + .safeMessage(true) + .build(); + } throw badRequest("Invalid protocol and/or version", method.text(), path, protocol, ""); } @@ -232,6 +253,8 @@ private String readProtocol(byte[] bytes, int index) { long word = Bytes.toWord(bytes, index); if (word == HTTP_1_1_LONG) { return HTTP_1_1; + } else if (word == HTTP_1_0_LONG) { + return HTTP_1_0; } } diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/ConnectionHandlerTest.java b/webserver/webserver/src/test/java/io/helidon/webserver/ConnectionHandlerTest.java new file mode 100644 index 00000000000..98790553229 --- /dev/null +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ConnectionHandlerTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 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.webserver; + +import java.nio.charset.StandardCharsets; + +import io.helidon.common.buffers.DataReader; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class ConnectionHandlerTest { + + @Test + void testHttp10Prologue() { + DataReader reader = new DataReader(() -> "GET / HTTP/1.0\r\n".getBytes(StandardCharsets.US_ASCII)); + assertThat(ConnectionHandler.isHttp10Connection(reader), is(true)); + } +} diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/http1/Http1PrologueTest.java b/webserver/webserver/src/test/java/io/helidon/webserver/http1/Http1PrologueTest.java index d4fc947225a..9749a94e34d 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/http1/Http1PrologueTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/http1/Http1PrologueTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -27,9 +27,11 @@ import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; class Http1PrologueTest { @Test @@ -55,4 +57,19 @@ void testUriTooLong() { assertThat(e.status(), is(Status.REQUEST_URI_TOO_LONG_414)); assertThat(e.eventType(), is(DirectHandler.EventType.BAD_REQUEST)); } + + @Test + void testHttp10Error() { + DataReader reader = new DataReader(() -> "GET / HTTP/1.0\r\n".getBytes(StandardCharsets.US_ASCII)); + Http1Prologue p = new Http1Prologue(reader, 100, false); + + try { + p.readPrologue(); + fail(); // exception not thrown + } catch (RequestException e) { + assertThat(e.status(), is(Status.HTTP_VERSION_NOT_SUPPORTED_505)); + assertThat(e.safeMessage(), is(true)); + assertThat(e.getMessage(), containsString("HTTP 1.0 is not supported")); + } + } } \ No newline at end of file