From dc26d362d25df8a0212c9c9bcc2e1d732010c947 Mon Sep 17 00:00:00 2001 From: Andrei Arlou Date: Thu, 13 Jun 2024 20:11:44 +0300 Subject: [PATCH 001/190] 4.x: Replace deprecated method Header.value() on Header.get() (#8873) --- .../testing/http/junit5/HttpHeaderMatcher.java | 4 ++-- .../security/basicauth/BasicExampleTest.java | 2 +- .../examples/webserver/echo/EchoClient.java | 4 ++-- .../encoding/ContentEncodingSupportImpl.java | 4 ++-- .../helidon/http/ClientResponseHeadersImpl.java | 4 ++-- .../io/helidon/http/ServerRequestHeaders.java | 10 +++++----- .../helidon/http/ServerRequestHeadersImpl.java | 2 +- .../io/helidon/http/ContentDispositionTest.java | 4 ++-- .../java/io/helidon/http/http2/Http2Headers.java | 6 +++--- .../io/helidon/http/http2/Http2HeadersTest.java | 14 +++++++------- .../media/multipart/ReadablePartAbstract.java | 4 ++-- .../integrations/common/rest/RestApiTest.java | 4 ++-- .../webclient/http1/Http1CallChainBase.java | 2 +- .../http1/Http1CallOutputStreamChain.java | 8 ++++---- .../webclient/http1/RedirectionProcessor.java | 2 +- .../webclient/websocket/WsClientImpl.java | 4 ++-- .../helidon/webserver/cors/AbstractCorsTest.java | 16 ++++++++-------- .../webserver/http1/Http1ServerRequest.java | 2 +- .../helidon/webserver/websocket/WsUpgrader.java | 8 ++++---- 19 files changed, 52 insertions(+), 52 deletions(-) diff --git a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java index 49eb2b41651..a75c30f41c3 100644 --- a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java +++ b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/HttpHeaderMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -217,7 +217,7 @@ protected boolean matchesSafely(Headers httpHeaders) { if (httpHeaders.contains(name)) { Header headerValue = httpHeaders.get(name); if (headerValue.allValues().size() == 1) { - return valuesMatcher.matches(headerValue.value()); + return valuesMatcher.matches(headerValue.get()); } return false; } diff --git a/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java index 34874e6ab2c..37a9a72414d 100644 --- a/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java +++ b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java @@ -132,7 +132,7 @@ private void testNotAuthorized(String uri) { try (Http1ClientResponse response = client.get().uri(uri).request()) { assertThat(response.status(), is(Status.UNAUTHORIZED_401)); - String header = response.headers().get(HeaderNames.WWW_AUTHENTICATE).value(); + String header = response.headers().get(HeaderNames.WWW_AUTHENTICATE).get(); assertThat(header.toLowerCase(), containsString("basic")); assertThat(header, containsString("helidon")); diff --git a/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java index 933fa093bfb..0460ff49fe3 100644 --- a/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java +++ b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -53,7 +53,7 @@ public static void main(String[] args) { Headers headers = response.headers(); for (Header header : headers) { - System.out.println("Header: " + header.name() + "=" + header.value()); + System.out.println("Header: " + header.name() + "=" + header.get()); } System.out.println("Entity:"); System.out.println(response.as(String.class)); diff --git a/http/encoding/encoding/src/main/java/io/helidon/http/encoding/ContentEncodingSupportImpl.java b/http/encoding/encoding/src/main/java/io/helidon/http/encoding/ContentEncodingSupportImpl.java index 2b52b6da4d2..0215f447d3b 100644 --- a/http/encoding/encoding/src/main/java/io/helidon/http/encoding/ContentEncodingSupportImpl.java +++ b/http/encoding/encoding/src/main/java/io/helidon/http/encoding/ContentEncodingSupportImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -120,7 +120,7 @@ public ContentEncoder encoder(Headers headers) { return ContentEncoder.NO_OP; } - String acceptEncoding = headers.get(HeaderNames.ACCEPT_ENCODING).value(); + String acceptEncoding = headers.get(HeaderNames.ACCEPT_ENCODING).get(); /* Accept-Encoding: gzip Accept-Encoding: gzip, compress, br diff --git a/http/http/src/main/java/io/helidon/http/ClientResponseHeadersImpl.java b/http/http/src/main/java/io/helidon/http/ClientResponseHeadersImpl.java index 3b90ec2969a..1ed604d5185 100644 --- a/http/http/src/main/java/io/helidon/http/ClientResponseHeadersImpl.java +++ b/http/http/src/main/java/io/helidon/http/ClientResponseHeadersImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -56,7 +56,7 @@ public Header get(HeaderName name) { public Optional contentType() { if (parserMode == ParserMode.RELAXED) { return contains(HeaderNameEnum.CONTENT_TYPE) - ? Optional.of(HttpMediaType.create(get(HeaderNameEnum.CONTENT_TYPE).value(), parserMode)) + ? Optional.of(HttpMediaType.create(get(HeaderNameEnum.CONTENT_TYPE).get(), parserMode)) : Optional.empty(); } return headers.contentType(); diff --git a/http/http/src/main/java/io/helidon/http/ServerRequestHeaders.java b/http/http/src/main/java/io/helidon/http/ServerRequestHeaders.java index 38cd32c9b9a..528b21e1af0 100644 --- a/http/http/src/main/java/io/helidon/http/ServerRequestHeaders.java +++ b/http/http/src/main/java/io/helidon/http/ServerRequestHeaders.java @@ -78,7 +78,7 @@ static ServerRequestHeaders create() { default Optional ifModifiedSince() { if (contains(HeaderNames.IF_MODIFIED_SINCE)) { return Optional.of(get(HeaderNames.IF_MODIFIED_SINCE)) - .map(Header::value) + .map(Header::get) .map(DateTime::parse); } @@ -95,7 +95,7 @@ default Optional ifModifiedSince() { default Optional ifUnmodifiedSince() { if (contains(HeaderNames.IF_UNMODIFIED_SINCE)) { return Optional.of(get(HeaderNames.IF_UNMODIFIED_SINCE)) - .map(Header::value) + .map(Header::get) .map(DateTime::parse); } return Optional.empty(); @@ -189,7 +189,7 @@ default Parameters cookies() { default Optional acceptDatetime() { if (contains(HeaderNames.ACCEPT_DATETIME)) { return Optional.of(get(HeaderNames.ACCEPT_DATETIME)) - .map(Header::value) + .map(Header::get) .map(DateTime::parse); } return Optional.empty(); @@ -203,7 +203,7 @@ default Optional acceptDatetime() { default Optional date() { if (contains(HeaderNames.DATE)) { return Optional.of(get(HeaderNames.DATE)) - .map(Header::value) + .map(Header::get) .map(DateTime::parse); } return Optional.empty(); @@ -221,7 +221,7 @@ default Optional date() { default Optional referer() { if (contains(HeaderNames.REFERER)) { return Optional.of(get(HeaderNames.REFERER)) - .map(Header::value) + .map(Header::get) .map(URI::create); } return Optional.empty(); diff --git a/http/http/src/main/java/io/helidon/http/ServerRequestHeadersImpl.java b/http/http/src/main/java/io/helidon/http/ServerRequestHeadersImpl.java index 20b5d066645..ef48b992bca 100644 --- a/http/http/src/main/java/io/helidon/http/ServerRequestHeadersImpl.java +++ b/http/http/src/main/java/io/helidon/http/ServerRequestHeadersImpl.java @@ -77,7 +77,7 @@ public List acceptedTypes() { List acceptedTypes; List acceptValues = all(HeaderNames.ACCEPT, List::of); - if (acceptValues.size() == 1 && HUC_ACCEPT_DEFAULT.value().equals(acceptValues.get(0))) { + if (acceptValues.size() == 1 && HUC_ACCEPT_DEFAULT.get().equals(acceptValues.get(0))) { acceptedTypes = HUC_ACCEPT_DEFAULT_TYPES; } else { acceptedTypes = new ArrayList<>(5); diff --git a/http/http/src/test/java/io/helidon/http/ContentDispositionTest.java b/http/http/src/test/java/io/helidon/http/ContentDispositionTest.java index bef61a70b63..3a012cb3290 100644 --- a/http/http/src/test/java/io/helidon/http/ContentDispositionTest.java +++ b/http/http/src/test/java/io/helidon/http/ContentDispositionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -224,7 +224,7 @@ void testQuotes() { .filename("file.txt") .size(300) .build(); - assertThat(cd.value(), is(equalTo(template))); + assertThat(cd.get(), is(equalTo(template))); } @Test diff --git a/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java b/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java index 62c9742a993..87c9537fc56 100644 --- a/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java +++ b/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java @@ -453,7 +453,7 @@ public void write(DynamicTable table, Http2HuffmanEncoder huffman, BufferData gr } for (Header header : headers) { - String value = header.value(); + String value = header.get(); boolean shouldIndex = !header.changing(); boolean neverIndex = header.sensitive(); @@ -649,7 +649,7 @@ private static Http2Headers createFromWritable(WritableHeaders headers) { headers.remove(HeaderNames.HOST, it -> { if (!pseudoHeaders.hasAuthority()) { - pseudoHeaders.authority(it.value()); + pseudoHeaders.authority(it.get()); } }); @@ -689,7 +689,7 @@ the stream (see Section 5.3). Add one to the value to obtain a private static void removeFromHeadersAddToPseudo(WritableHeaders headers, Consumer valueConsumer, HeaderName pseudoHeader) { - headers.remove(pseudoHeader, it -> valueConsumer.accept(it.value())); + headers.remove(pseudoHeader, it -> valueConsumer.accept(it.get())); } private void writeHeader(Http2HuffmanEncoder huffman, DynamicTable table, diff --git a/http/http2/src/test/java/io/helidon/http/http2/Http2HeadersTest.java b/http/http2/src/test/java/io/helidon/http/http2/Http2HeadersTest.java index 1aa703f9337..f27d7d77eb5 100644 --- a/http/http2/src/test/java/io/helidon/http/http2/Http2HeadersTest.java +++ b/http/http2/src/test/java/io/helidon/http/http2/Http2HeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -46,7 +46,7 @@ void testC_2_1() { DynamicTable dynamicTable = DynamicTable.create(Http2Settings.create()); Headers requestHeaders = headers(hexEncoded, dynamicTable).httpHeaders(); - assertThat(requestHeaders.get(HeaderNames.create("custom-key")).value(), is("custom-header")); + assertThat(requestHeaders.get(HeaderNames.create("custom-key")).get(), is("custom-header")); } BufferData data(String hexEncoded) { @@ -76,7 +76,7 @@ void testC_2_3() { DynamicTable dynamicTable = DynamicTable.create(Http2Settings.create()); Headers requestHeaders = headers(hexEncoded, dynamicTable).httpHeaders(); - assertThat(requestHeaders.get(HeaderNames.create("password")).value(), is("secret")); + assertThat(requestHeaders.get(HeaderNames.create("password")).get(), is("secret")); assertThat("Dynamic table should be empty", dynamicTable.currentTableSize(), is(0)); } @@ -124,7 +124,7 @@ void testC_3() { assertThat(http2Headers.scheme(), is("http")); assertThat(http2Headers.path(), is("/")); assertThat(http2Headers.authority(), is("www.example.com")); - assertThat(requestHeaders.get(HeaderNames.create("cache-control")).value(), is("no-cache")); + assertThat(requestHeaders.get(HeaderNames.create("cache-control")).get(), is("no-cache")); assertThat("Dynamic table should not be empty", dynamicTable.currentTableSize(), not(0)); @@ -145,7 +145,7 @@ void testC_3() { assertThat(http2Headers.scheme(), is("https")); assertThat(http2Headers.path(), is("/index.html")); assertThat(http2Headers.authority(), is("www.example.com")); - assertThat(requestHeaders.get(CUSTOM_HEADER_NAME).value(), is("custom-value")); + assertThat(requestHeaders.get(CUSTOM_HEADER_NAME).get(), is("custom-value")); assertThat("Dynamic table should not be empty", dynamicTable.currentTableSize(), not(0)); @@ -214,7 +214,7 @@ void testC_4() { assertThat(http2Headers.scheme(), is("http")); assertThat(http2Headers.path(), is("/")); assertThat(http2Headers.authority(), is("www.example.com")); - assertThat(requestHeaders.get(HeaderNames.create("cache-control")).value(), is("no-cache")); + assertThat(requestHeaders.get(HeaderNames.create("cache-control")).get(), is("no-cache")); assertThat("Dynamic table should not be empty", dynamicTable.currentTableSize(), not(0)); @@ -235,7 +235,7 @@ void testC_4() { assertThat(http2Headers.scheme(), is("https")); assertThat(http2Headers.path(), is("/index.html")); assertThat(http2Headers.authority(), is("www.example.com")); - assertThat(requestHeaders.get(CUSTOM_HEADER_NAME).value(), is("custom-value")); + assertThat(requestHeaders.get(CUSTOM_HEADER_NAME).get(), is("custom-value")); assertThat("Dynamic table should not be empty", dynamicTable.currentTableSize(), not(0)); diff --git a/http/media/multipart/src/main/java/io/helidon/http/media/multipart/ReadablePartAbstract.java b/http/media/multipart/src/main/java/io/helidon/http/media/multipart/ReadablePartAbstract.java index f1e6a1bcc27..11d53a8a469 100644 --- a/http/media/multipart/src/main/java/io/helidon/http/media/multipart/ReadablePartAbstract.java +++ b/http/media/multipart/src/main/java/io/helidon/http/media/multipart/ReadablePartAbstract.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -80,7 +80,7 @@ public boolean hasEntity() { private void contentDisposition() { if (headers.contains(HeaderNames.CONTENT_DISPOSITION)) { - this.contentDisposition = ContentDisposition.parse(headers.get(HeaderNames.CONTENT_DISPOSITION).value()); + this.contentDisposition = ContentDisposition.parse(headers.get(HeaderNames.CONTENT_DISPOSITION).get()); } else { this.contentDisposition = ContentDisposition.empty(); } diff --git a/integrations/common/rest/src/test/java/io/helidon/integrations/common/rest/RestApiTest.java b/integrations/common/rest/src/test/java/io/helidon/integrations/common/rest/RestApiTest.java index 145812c4fff..32165b38c9d 100644 --- a/integrations/common/rest/src/test/java/io/helidon/integrations/common/rest/RestApiTest.java +++ b/integrations/common/rest/src/test/java/io/helidon/integrations/common/rest/RestApiTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 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. @@ -158,7 +158,7 @@ private JsonObjectBuilder common(ServerRequest req) { ServerRequestHeaders headers = req.headers(); if (headers.size() > 0) { JsonObjectBuilder headersBuilder = JSON.createObjectBuilder(); - headers.forEach(header -> headersBuilder.add(header.name(), header.value())); + headers.forEach(header -> headersBuilder.add(header.name(), header.get())); objectBuilder.add("headers", headersBuilder); } diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallChainBase.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallChainBase.java index bd7e0db61cc..efc20d457d2 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallChainBase.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallChainBase.java @@ -250,7 +250,7 @@ private static InputStream inputStream(HttpClientConfig clientConfig, ContentDecoder decoder; if (encodingSupport.contentDecodingEnabled() && responseHeaders.contains(HeaderNames.CONTENT_ENCODING)) { - String contentEncoding = responseHeaders.get(HeaderNames.CONTENT_ENCODING).value(); + String contentEncoding = responseHeaders.get(HeaderNames.CONTENT_ENCODING).get(); if (encodingSupport.contentDecodingSupported(contentEncoding)) { decoder = encodingSupport.decoder(contentEncoding); } else { diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java index 78fb1206a7f..1532f9d4777 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1CallOutputStreamChain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 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. @@ -130,7 +130,7 @@ WebClientServiceResponse doProceed(ClientConnection connection, if (originalRequest().followRedirects() && RedirectionProcessor.redirectionStatusCode(responseStatus)) { checkRedirectHeaders(responseHeaders); - URI newUri = URI.create(responseHeaders.get(HeaderNames.LOCATION).value()); + URI newUri = URI.create(responseHeaders.get(HeaderNames.LOCATION).get()); ClientUri redirectUri = ClientUri.create(newUri); if (newUri.getHost() == null) { UriInfo resolvedUri = cos.lastRequest.resolvedUri(); @@ -440,7 +440,7 @@ private void sendPrologueAndHeader() { } private void redirect(Status lastStatus, WritableHeaders headerValues) { - String redirectedUri = headerValues.get(HeaderNames.LOCATION).value(); + String redirectedUri = headerValues.get(HeaderNames.LOCATION).get(); ClientUri lastUri = originalRequest.uri(); Method method; boolean sendEntity; @@ -492,7 +492,7 @@ private void redirect(Status lastStatus, WritableHeaders headerValues) { method = Method.GET; sendEntity = false; } - redirectedUri = response.headers().get(HeaderNames.LOCATION).value(); + redirectedUri = response.headers().get(HeaderNames.LOCATION).get(); } } else { if (!sendEntity) { diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/RedirectionProcessor.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/RedirectionProcessor.java index 0ea7b39d733..4ae672c6dcc 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/RedirectionProcessor.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/RedirectionProcessor.java @@ -56,7 +56,7 @@ static Http1ClientResponseImpl invokeWithFollowRedirects(Http1ClientRequestImpl + " header present in the response! " + "It is not clear where to redirect."); } - String redirectedUri = clientResponse.headers().get(HeaderNames.LOCATION).value(); + String redirectedUri = clientResponse.headers().get(HeaderNames.LOCATION).get(); URI newUri = URI.create(redirectedUri); ClientUri redirectUri = ClientUri.create(newUri); diff --git a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientImpl.java b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientImpl.java index 63c3a58615c..ee09ea95835 100644 --- a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientImpl.java +++ b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientImpl.java @@ -142,14 +142,14 @@ public void connect(URI uri, WsListener listener) { + responseHeaders); } ClientConnection connection = upgradeResponse.connection(); - String secWsAccept = responseHeaders.get(HEADER_WS_ACCEPT).value(); + String secWsAccept = responseHeaders.get(HEADER_WS_ACCEPT).get(); if (!hash(connection.helidonSocket(), secWsKey).equals(secWsAccept)) { throw new WsClientException("Failed to upgrade to WebSocket, expected valid secWsKey. Headers: " + responseHeaders); } // we are upgraded, let's switch to web socket if (headers.contains(HEADER_WS_PROTOCOL)) { - session = new ClientWsConnection(connection, listener, headers.get(HEADER_WS_PROTOCOL).value()); + session = new ClientWsConnection(connection, listener, headers.get(HEADER_WS_PROTOCOL).get()); } else { session = new ClientWsConnection(connection, listener); } diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java index d484aca0fc4..b2d3e146d35 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 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. @@ -178,11 +178,11 @@ void test2PreFlightAllowedHeaders1() { assertThat(response.status(), is(Status.OK_200)); assertThat(response.headers() - .get(ACCESS_CONTROL_ALLOW_ORIGIN).value(), is(fooOrigin())); + .get(ACCESS_CONTROL_ALLOW_ORIGIN).get(), is(fooOrigin())); assertThat(response.headers() - .get(ACCESS_CONTROL_ALLOW_CREDENTIALS).value(), is("true")); + .get(ACCESS_CONTROL_ALLOW_CREDENTIALS).get(), is("true")); assertThat(response.headers() - .get(ACCESS_CONTROL_ALLOW_METHODS).value(), is("PUT")); + .get(ACCESS_CONTROL_ALLOW_METHODS).get(), is("PUT")); assertThat(response.headers() .get(ACCESS_CONTROL_ALLOW_HEADERS).values(), containsString(fooHeader())); assertThat(response.headers(), noHeader(ACCESS_CONTROL_MAX_AGE)); @@ -204,8 +204,8 @@ void test2PreFlightAllowedHeaders2() { assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "http://foo.bar")); assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_METHODS, "PUT")); - assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).value(), containsString("X-foo")); - assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).value(), containsString("X-bar")); + assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).get(), containsString("X-foo")); + assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).get(), containsString("X-bar")); assertThat(response.headers(), noHeader(ACCESS_CONTROL_MAX_AGE)); } } @@ -226,8 +226,8 @@ void test2PreFlightAllowedHeaders3() { assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "http://foo.bar")); assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); assertThat(response.headers(), hasHeader(ACCESS_CONTROL_ALLOW_METHODS, "PUT")); - assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).value(), containsString("X-foo")); - assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).value(), containsString("X-bar")); + assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).get(), containsString("X-foo")); + assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS).get(), containsString("X-bar")); assertThat(response.headers(), noHeader(ACCESS_CONTROL_MAX_AGE)); } } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java index 84a7a1e5e9c..86a29e7a8de 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java @@ -182,7 +182,7 @@ public PeerInfo localPeer() { @Override public String authority() { - return headers.get(HeaderNames.HOST).value(); + return headers.get(HeaderNames.HOST).get(); } @Override diff --git a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsUpgrader.java b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsUpgrader.java index 71f21ca49a0..997e29518f4 100644 --- a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsUpgrader.java +++ b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsUpgrader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -124,7 +124,7 @@ public String supportedProtocol() { public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders headers) { String wsKey; if (headers.contains(WS_KEY)) { - wsKey = headers.get(WS_KEY).value(); + wsKey = headers.get(WS_KEY).get(); } else { // this header is required return null; @@ -132,7 +132,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, Wr // protocol version String version; if (headers.contains(WS_VERSION)) { - version = headers.get(WS_VERSION).value(); + version = headers.get(WS_VERSION).get(); } else { version = SUPPORTED_VERSION; } @@ -156,7 +156,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, Wr if (!anyOrigin()) { if (headers.contains(HeaderNames.ORIGIN)) { - String origin = headers.get(HeaderNames.ORIGIN).value(); + String origin = headers.get(HeaderNames.ORIGIN).get(); if (!origins().contains(origin)) { throw RequestException.builder() .message("Invalid Origin") From 1ded39ac76228f7434aaf38d4a0a67891bdbf5b4 Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Thu, 13 Jun 2024 19:19:56 -0500 Subject: [PATCH 002/190] Correct the ordering of whenSent in doc snippet (#8884) Signed-off-by: Tim Quinn --- .../main/java/io/helidon/docs/se/guides/MetricsSnippets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/java/io/helidon/docs/se/guides/MetricsSnippets.java b/docs/src/main/java/io/helidon/docs/se/guides/MetricsSnippets.java index c38046ee399..c460c3229b5 100644 --- a/docs/src/main/java/io/helidon/docs/se/guides/MetricsSnippets.java +++ b/docs/src/main/java/io/helidon/docs/se/guides/MetricsSnippets.java @@ -174,8 +174,8 @@ public void routing(HttpRules rules) { private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { Timer.Sample timerSample = Timer.start(); // <3> - sendResponse(response, "Here are some cards ..."); response.whenSent(() -> timerSample.stop(cardTimer)); // <4> + sendResponse(response, "Here are some cards ..."); } private void sendResponse(ServerResponse response, String msg) { From ed37d4fee4ff5acee1f2109143b78391e5b83d64 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Mon, 17 Jun 2024 21:00:57 +0200 Subject: [PATCH 003/190] Fix multi-value query string parsing (#8889) * Fix multi-value query string parsing * Fixed handling of query methods. Reverted to use raw query (as intended), and fixed javadoc of the method. Added tests. --- .../helidon/common/uri/UriQueryWriteable.java | 9 ++++++--- .../common/uri/UriQueryWriteableImpl.java | 14 ++++++++++++-- .../io/helidon/common/uri/UriQueryTest.java | 17 +++++++++++++++-- .../io/helidon/webclient/api/ClientUri.java | 6 ++---- .../io/helidon/webclient/api/ClientUriTest.java | 16 +++++++++++++++- 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java index c8318896314..12310b8749f 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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,8 +85,11 @@ static UriQueryWriteable create() { UriQueryWriteable remove(String name, Consumer> removedConsumer); /** - * Update from a query string (with decoded values). - * @param queryString decoded query string to update this instance + * Update from a query string (with encoded values). + *

+ * This documentation (and behavior) has been changed, as we cannot create a proper query from {@code decoded} values, + * as these may contain characters used to split the query. + * @param queryString encoded query string to update this instance */ void fromQueryString(String queryString); diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java index 2dc04a51670..b6c2ba45aa2 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java @@ -292,11 +292,21 @@ public String toString() { private void addRaw(String next) { int eq = next.indexOf('='); if (eq == -1) { - set(next); + addRaw(next, ""); } else { String name = next.substring(0, eq); String value = next.substring(eq + 1); - set(name, value); + addRaw(name, value); } } + + private void addRaw(String encodedName, String encodedValue) { + String decodedName = UriEncoding.decodeUri(encodedName); + String decodedValue = UriEncoding.decodeUri(encodedValue); + + rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(1)) + .add(encodedValue); + decodedQueryParams.computeIfAbsent(decodedName, it -> new ArrayList<>(1)) + .add(decodedValue); + } } diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java index edb01dd62bd..d9dc16fed24 100644 --- a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java +++ b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java @@ -57,7 +57,7 @@ void testEncodedOtherChars() { assertThat(uriQuery.get("h"), is("xc#e<")); assertThat(uriQuery.get("a"), is("b&c=d")); } - + @Test void testEmptyQueryString() { UriQuery uriQuery = UriQuery.create(""); @@ -80,10 +80,23 @@ void issue8710() { UriQuery uriQuery = UriQuery.create(URI.create("http://foo/bar?a&b=c")); OptionalValue optional = uriQuery.first("a"); assertThat(optional.isEmpty(), is(true)); - + assertThat(uriQuery.all("a"), hasItems()); assertThat(uriQuery.all("b"), hasItems("c")); assertThat(uriQuery.getRaw("a"), is("")); } + @Test + void testFromQueryString() { + UriQueryWriteable query = UriQueryWriteable.create(); + query.fromQueryString("p1=v1&p2=v2&p3=%2F%2Fv3%2F%2F&p4=a%20b%20c"); + assertThat(query.get("p1"), is("v1")); + assertThat(query.get("p2"), is("v2")); + assertThat(query.get("p3"), is("//v3//")); + // make sure the encoded value is correct + assertThat(query.getRaw("p3"), is("%2F%2Fv3%2F%2F")); + assertThat(query.get("p4"), is("a b c")); + assertThat(query.getRaw("p4"), is("a%20b%20c")); + } + } \ No newline at end of file diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/ClientUri.java b/webclient/api/src/main/java/io/helidon/webclient/api/ClientUri.java index 9903579584a..c0f5403f52d 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/ClientUri.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/ClientUri.java @@ -18,7 +18,6 @@ import java.net.URI; -import io.helidon.common.uri.UriEncoding; import io.helidon.common.uri.UriFragment; import io.helidon.common.uri.UriInfo; import io.helidon.common.uri.UriPath; @@ -194,11 +193,10 @@ public ClientUri resolve(URI uri) { uriBuilder.path(resolvePath(uriBuilder.path().path(), uri.getPath())); - String queryString = uri.getQuery(); + String queryString = uri.getRawQuery(); if (queryString != null) { // class URI does not decode +'s, so we do it here - query.fromQueryString(queryString.indexOf('+') >= 0 ? UriEncoding.decodeUri(queryString) - : queryString); + query.fromQueryString(queryString.replaceAll("\\+", "%20")); } if (uri.getRawFragment() != null) { diff --git a/webclient/api/src/test/java/io/helidon/webclient/api/ClientUriTest.java b/webclient/api/src/test/java/io/helidon/webclient/api/ClientUriTest.java index 56c29a00d7b..a670bc45586 100644 --- a/webclient/api/src/test/java/io/helidon/webclient/api/ClientUriTest.java +++ b/webclient/api/src/test/java/io/helidon/webclient/api/ClientUriTest.java @@ -20,8 +20,10 @@ import io.helidon.common.uri.UriPath; import io.helidon.common.uri.UriQueryWriteable; + import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -63,7 +65,12 @@ void testNonDefaults() { void testQueryParams() { UriQueryWriteable query = UriQueryWriteable.create(); query.fromQueryString("p1=v1&p2=v2&p3=%2F%2Fv3%2F%2F"); - ClientUri helper = ClientUri.create(URI.create("http://localhost:8080/loom/quick?" + query.value())); + assertThat(query.get("p1"), is("v1")); + assertThat(query.get("p2"), is("v2")); + assertThat(query.get("p3"), is("//v3//")); + assertThat(query.getRaw("p3"), is("%2F%2Fv3%2F%2F")); + + ClientUri helper = ClientUri.create(URI.create("http://localhost:8080/loom/quick?" + query.rawValue())); assertThat(helper.authority(), is("localhost:8080")); assertThat(helper.host(), is("localhost")); @@ -76,6 +83,13 @@ void testQueryParams() { assertThat(helper.query().getRaw("p3"), is("%2F%2Fv3%2F%2F")); } + @Test + void testQueryMultipleValues() { + UriQueryWriteable query = UriQueryWriteable.create(); + query.fromQueryString("p1=v1&p1=v2"); + assertThat(query.all("p1"), hasItems("v1", "v2")); + } + @Test void testResolvePath() { ClientUri helper = ClientUri.create(URI.create("http://localhost:8080/loom")); From d94706db173727d8b575dce5ffa4dbcb377df4c5 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Mon, 17 Jun 2024 23:32:07 +0200 Subject: [PATCH 004/190] 4.x: Fixed configuration metadata of blueprints that are configured and provide a service (#8891) * Fixed configuration metadata of protocol configs. * Updated generated documentation to latest state of code. * Fixed additional providers blueprints to add config key. * Added validation of @Provides with @Configured - it must provide a configuration key now. * Validation errors now contain source type for better error messages. --- .../io/helidon/builder/codegen/Types.java | 1 + .../builder/codegen/ValidationTask.java | 26 +++++++++++++++---- .../io/helidon/builder/codegen/TypesTest.java | 1 + .../io_helidon_webserver_ListenerConfig.adoc | 11 +++++++- .../io_helidon_webserver_WebServer.adoc | 11 +++++++- .../io_helidon_webserver_cors_CorsConfig.adoc | 2 +- ...io_helidon_webserver_cors_CorsFeature.adoc | 2 +- .../io_helidon_webserver_grpc_GrpcConfig.adoc | 10 +++++-- ...o_helidon_webserver_http1_Http1Config.adoc | 8 +++++- ...o_helidon_webserver_http2_Http2Config.adoc | 8 +++++- ...idon_webserver_observe_ObserveFeature.adoc | 8 +++--- ...bserver_observe_config_ConfigObserver.adoc | 6 +++++ ...n_webserver_observe_info_InfoObserver.adoc | 6 +++++ ...don_webserver_observe_log_LogObserver.adoc | 6 +++++ ...erver_observe_tracing_TracingObserver.adoc | 6 +++++ ..._helidon_webserver_websocket_WsConfig.adoc | 8 +++++- .../webserver/grpc/GrpcConfigBlueprint.java | 5 ++-- .../webserver/http2/Http2ConfigBlueprint.java | 5 ++-- .../config/ConfigObserverConfigBlueprint.java | 4 +-- .../info/InfoObserverConfigBlueprint.java | 4 +-- .../log/LogObserverConfigBlueprint.java | 4 +-- webserver/observe/tracing/pom.xml | 5 ++++ .../TracingObserverConfigBlueprint.java | 5 ++-- .../webserver/http1/Http1ConfigBlueprint.java | 5 ++-- .../websocket/WsConfigBlueprint.java | 5 ++-- 25 files changed, 128 insertions(+), 34 deletions(-) diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java index 8a274abb2cb..3fb26ec7436 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java @@ -43,6 +43,7 @@ final class Types { static final TypeName PROTOTYPE_ANNOTATED = TypeName.create("io.helidon.builder.api.Prototype.Annotated"); static final TypeName PROTOTYPE_FACTORY = TypeName.create("io.helidon.builder.api.Prototype.Factory"); static final TypeName PROTOTYPE_CONFIGURED = TypeName.create("io.helidon.builder.api.Prototype.Configured"); + static final TypeName PROTOTYPE_PROVIDES = TypeName.create("io.helidon.builder.api.Prototype.Provides"); static final TypeName PROTOTYPE_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.Builder"); static final TypeName PROTOTYPE_CONFIGURED_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.ConfiguredBuilder"); static final TypeName PROTOTYPE_CUSTOM_METHODS = TypeName.create("io.helidon.builder.api.Prototype.CustomMethods"); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java index f0aa71335cb..f9ff370e4a1 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java @@ -23,6 +23,7 @@ import io.helidon.codegen.ElementInfoPredicates; import io.helidon.common.Errors; import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; @@ -41,7 +42,7 @@ private static void validateImplements(Errors.Collector errors, if (validatedType.interfaceTypeInfo() .stream() .noneMatch(it -> it.typeName().equals(implementedInterface))) { - errors.fatal(message); + errors.fatal(validatedType.typeName(), message); } } @@ -70,7 +71,7 @@ private static void validateFactoryMethod(Errors.Collector errors, }) .findFirst() .isEmpty()) { - errors.fatal(validatedType.typeName().fqName(), message); + errors.fatal(validatedType.typeName(), message); } } @@ -166,7 +167,21 @@ static class ValidateBlueprint extends ValidationTask { public void validate(Errors.Collector errors) { // must be package local if (blueprint.accessModifier() == AccessModifier.PUBLIC) { - errors.fatal(blueprint.typeName().fqName() + " is defined as public, it must be package local"); + errors.fatal(blueprint.typeName(), blueprint.typeName().fqName() + + " is defined as public, it must be package local"); + } + + // if configured & provides, must have config key + if (blueprint.hasAnnotation(Types.PROTOTYPE_CONFIGURED) + && blueprint.hasAnnotation(Types.PROTOTYPE_PROVIDES)) { + Annotation configured = blueprint.annotation(Types.PROTOTYPE_CONFIGURED); + String value = configured.stringValue().orElse(""); + if (value.isEmpty()) { + // we have a @Configured and @Provides - this should have a configuration key! + errors.fatal(blueprint.typeName(), blueprint.typeName().fqName() + + " is marked as @Configured and @Provides, yet it does not" + + " define a configuration key"); + } } } @@ -276,8 +291,9 @@ void validate(Errors.Collector errors) { if (typeInfo.findAnnotation(annotation) .stream() .noneMatch(it -> it.value().map(expectedValue::equals).orElse(false))) { - errors.fatal("Type " + typeInfo.typeName() - .fqName() + " must be annotated with " + annotation.fqName() + "(" + expectedValue + ")"); + errors.fatal(typeInfo.typeName(), + "Type " + typeInfo.typeName().fqName() + + " must be annotated with " + annotation.fqName() + "(" + expectedValue + ")"); } } } diff --git a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java index f5c1d35db9c..f0d2714df0c 100644 --- a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java +++ b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java @@ -94,6 +94,7 @@ void testTypes() { checkField(toCheck, checked, fields, "PROTOTYPE_ANNOTATED", Prototype.Annotated.class); checkField(toCheck, checked, fields, "PROTOTYPE_FACTORY", Prototype.Factory.class); checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED", Prototype.Configured.class); + checkField(toCheck, checked, fields, "PROTOTYPE_PROVIDES", Prototype.Provides.class); checkField(toCheck, checked, fields, "PROTOTYPE_BUILDER", Prototype.Builder.class); checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED_BUILDER", Prototype.ConfiguredBuilder.class); checkField(toCheck, checked, fields, "PROTOTYPE_CUSTOM_METHODS", Prototype.CustomMethods.class); diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_ListenerConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_ListenerConfig.adoc index 5ab27735bf5..a77b8f500ba 100644 --- a/docs/src/main/asciidoc/config/io_helidon_webserver_ListenerConfig.adoc +++ b/docs/src/main/asciidoc/config/io_helidon_webserver_ListenerConfig.adoc @@ -110,7 +110,16 @@ Type: link:{javadoc-base-url}/io.helidon.webserver/io/helidon/webserver/Listener If configured to `0` (the default), server starts on a random port. Port to listen on (for the default socket) -|`protocols` |io.helidon.webserver.spi.ProtocolConfig[] (service provider interface) |{nbsp} |Configuration of protocols. This may be either protocol selectors, or protocol upgraders from HTTP/1.1. +|`protocols` |io.helidon.webserver.spi.ProtocolConfig[] (service provider interface) + +Such as: + + - xref:{rootdir}/config/io_helidon_webserver_http2_Http2Config.adoc[http_2 (Http2Config)] + - xref:{rootdir}/config/io_helidon_webserver_grpc_GrpcConfig.adoc[grpc (GrpcConfig)] + - xref:{rootdir}/config/io_helidon_webserver_websocket_WsConfig.adoc[websocket (WsConfig)] + - xref:{rootdir}/config/io_helidon_webserver_http1_Http1Config.adoc[http_1_1 (Http1Config)] + + |{nbsp} |Configuration of protocols. This may be either protocol selectors, or protocol upgraders from HTTP/1.1. As the order is not important (providers are ordered by weight by default), we can use a configuration as an object, such as:

diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_WebServer.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_WebServer.adoc
index cd8f542191c..666d8727631 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_WebServer.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_WebServer.adoc
@@ -126,7 +126,16 @@ Such as:
  If configured to `0` (the default), server starts on a random port.
 
  Port to listen on (for the default socket)
-|`protocols` |io.helidon.webserver.spi.ProtocolConfig[] (service provider interface) |{nbsp} |Configuration of protocols. This may be either protocol selectors, or protocol upgraders from HTTP/1.1.
+|`protocols` |io.helidon.webserver.spi.ProtocolConfig[] (service provider interface)
+
+Such as:
+
+ - xref:{rootdir}/config/io_helidon_webserver_http2_Http2Config.adoc[http_2 (Http2Config)]
+ - xref:{rootdir}/config/io_helidon_webserver_grpc_GrpcConfig.adoc[grpc (GrpcConfig)]
+ - xref:{rootdir}/config/io_helidon_webserver_websocket_WsConfig.adoc[websocket (WsConfig)]
+ - xref:{rootdir}/config/io_helidon_webserver_http1_Http1Config.adoc[http_1_1 (Http1Config)]
+
+ |{nbsp} |Configuration of protocols. This may be either protocol selectors, or protocol upgraders from HTTP/1.1.
  As the order is not important (providers are ordered by weight by default), we can use a configuration as an object,
  such as:
  
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsConfig.adoc
index 9531104cc8b..c57c843420d 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsConfig.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsConfig.adoc
@@ -66,7 +66,7 @@ This type provides the following service implementations:
 |`sockets` |string[] |{nbsp} |List of sockets to register this feature on. If empty, it would get registered on all sockets.
 
  Socket names to register on, defaults to empty (all available sockets)
-|`weight` |double |`950.0` |Weight of the CORS feature. As it is used by other features, the default is quite high:
+|`weight` |double |`850.0` |Weight of the CORS feature. As it is used by other features, the default is quite high:
  CorsFeature.WEIGHT.
 
  Weight of the feature
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsFeature.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsFeature.adoc
index 9531104cc8b..c57c843420d 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsFeature.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_cors_CorsFeature.adoc
@@ -66,7 +66,7 @@ This type provides the following service implementations:
 |`sockets` |string[] |{nbsp} |List of sockets to register this feature on. If empty, it would get registered on all sockets.
 
  Socket names to register on, defaults to empty (all available sockets)
-|`weight` |double |`950.0` |Weight of the CORS feature. As it is used by other features, the default is quite high:
+|`weight` |double |`850.0` |Weight of the CORS feature. As it is used by other features, the default is quite high:
  CorsFeature.WEIGHT.
 
  Weight of the feature
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_grpc_GrpcConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_grpc_GrpcConfig.adoc
index 5df77b1b502..87d5f506f09 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_grpc_GrpcConfig.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_grpc_GrpcConfig.adoc
@@ -1,6 +1,6 @@
 ///////////////////////////////////////////////////////////////////////////////
 
-    Copyright (c) 2023 Oracle and/or its affiliates.
+    Copyright (c) 2023, 2024 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,10 +30,16 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.grpc/io/helidon/webserver/grpc/GrpcConfig.html[io.helidon.webserver.grpc.GrpcConfig]
 
 
+[source,text]
+.Config key
+----
+grpc
+----
+
 
 This type provides the following service implementations:
 
-- `io.helidon.webserver.spi.ProtocolConfig`
+- `io.helidon.webserver.spi.ProtocolConfigProvider`
 
 
 == Configuration options
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_http1_Http1Config.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_http1_Http1Config.adoc
index 7a8b58ea104..a72c8fbf80c 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_http1_Http1Config.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_http1_Http1Config.adoc
@@ -30,10 +30,16 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.http1/io/helidon/webserver/http1/Http1Config.html[io.helidon.webserver.http1.Http1Config]
 
 
+[source,text]
+.Config key
+----
+http_1_1
+----
+
 
 This type provides the following service implementations:
 
-- `io.helidon.webserver.spi.ProtocolConfig`
+- `io.helidon.webserver.spi.ProtocolConfigProvider`
 
 
 == Configuration options
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_http2_Http2Config.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_http2_Http2Config.adoc
index c6324a3b995..099e63003e3 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_http2_Http2Config.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_http2_Http2Config.adoc
@@ -30,10 +30,16 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.http2/io/helidon/webserver/http2/Http2Config.html[io.helidon.webserver.http2.Http2Config]
 
 
+[source,text]
+.Config key
+----
+http_2
+----
+
 
 This type provides the following service implementations:
 
-- `io.helidon.webserver.spi.ProtocolConfig`
+- `io.helidon.webserver.spi.ProtocolConfigProvider`
 
 
 == Configuration options
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_ObserveFeature.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_ObserveFeature.adoc
index 16f38a02e30..2559f02848a 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_ObserveFeature.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_ObserveFeature.adoc
@@ -70,10 +70,10 @@ This type provides the following service implementations:
 
 Such as:
 
- - xref:{rootdir}/config/io_helidon_webserver_observe_log_LogObserver.adoc[LogObserver]
- - xref:{rootdir}/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc[TracingObserver]
- - xref:{rootdir}/config/io_helidon_webserver_observe_config_ConfigObserver.adoc[ConfigObserver]
- - xref:{rootdir}/config/io_helidon_webserver_observe_info_InfoObserver.adoc[InfoObserver]
+ - xref:{rootdir}/config/io_helidon_webserver_observe_log_LogObserver.adoc[log (LogObserver)]
+ - xref:{rootdir}/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc[tracing (TracingObserver)]
+ - xref:{rootdir}/config/io_helidon_webserver_observe_config_ConfigObserver.adoc[config (ConfigObserver)]
+ - xref:{rootdir}/config/io_helidon_webserver_observe_info_InfoObserver.adoc[info (InfoObserver)]
  - xref:{rootdir}/config/io_helidon_webserver_observe_metrics_MetricsObserver.adoc[metrics (MetricsObserver)]
  - xref:{rootdir}/config/io_helidon_webserver_observe_health_HealthObserver.adoc[health (HealthObserver)]
 
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_config_ConfigObserver.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_config_ConfigObserver.adoc
index 43eb920fc73..ed70f806692 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_config_ConfigObserver.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_config_ConfigObserver.adoc
@@ -30,6 +30,12 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.observe.config/io/helidon/webserver/observe/config/ConfigObserver.html[io.helidon.webserver.observe.config.ConfigObserver]
 
 
+[source,text]
+.Config key
+----
+config
+----
+
 
 This type provides the following service implementations:
 
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_info_InfoObserver.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_info_InfoObserver.adoc
index 4016a71d1af..e56c148a15a 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_info_InfoObserver.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_info_InfoObserver.adoc
@@ -30,6 +30,12 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.observe.info/io/helidon/webserver/observe/info/InfoObserver.html[io.helidon.webserver.observe.info.InfoObserver]
 
 
+[source,text]
+.Config key
+----
+info
+----
+
 
 This type provides the following service implementations:
 
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_log_LogObserver.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_log_LogObserver.adoc
index 69402e92a7e..ff1d897bf07 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_log_LogObserver.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_log_LogObserver.adoc
@@ -30,6 +30,12 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.observe.log/io/helidon/webserver/observe/log/LogObserver.html[io.helidon.webserver.observe.log.LogObserver]
 
 
+[source,text]
+.Config key
+----
+log
+----
+
 
 This type provides the following service implementations:
 
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc
index fc0e267850e..feea2ca6abc 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_observe_tracing_TracingObserver.adoc
@@ -30,6 +30,12 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.observe.tracing/io/helidon/webserver/observe/tracing/TracingObserver.html[io.helidon.webserver.observe.tracing.TracingObserver]
 
 
+[source,text]
+.Config key
+----
+tracing
+----
+
 
 This type provides the following service implementations:
 
diff --git a/docs/src/main/asciidoc/config/io_helidon_webserver_websocket_WsConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_webserver_websocket_WsConfig.adoc
index 884ee43c457..304bd316def 100644
--- a/docs/src/main/asciidoc/config/io_helidon_webserver_websocket_WsConfig.adoc
+++ b/docs/src/main/asciidoc/config/io_helidon_webserver_websocket_WsConfig.adoc
@@ -30,10 +30,16 @@ include::{rootdir}/includes/attributes.adoc[]
 Type: link:{javadoc-base-url}/io.helidon.webserver.websocket/io/helidon/webserver/websocket/WsConfig.html[io.helidon.webserver.websocket.WsConfig]
 
 
+[source,text]
+.Config key
+----
+websocket
+----
+
 
 This type provides the following service implementations:
 
-- `io.helidon.webserver.spi.ProtocolConfig`
+- `io.helidon.webserver.spi.ProtocolConfigProvider`
 
 
 == Configuration options
diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java
index 286a8111bee..0593ee88fad 100644
--- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java
+++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java
@@ -18,10 +18,11 @@
 
 import io.helidon.builder.api.Prototype;
 import io.helidon.webserver.spi.ProtocolConfig;
+import io.helidon.webserver.spi.ProtocolConfigProvider;
 
 @Prototype.Blueprint
-@Prototype.Configured
-@Prototype.Provides(ProtocolConfig.class)
+@Prototype.Configured(root = false, value = GrpcProtocolProvider.CONFIG_NAME)
+@Prototype.Provides(ProtocolConfigProvider.class)
 interface GrpcConfigBlueprint extends ProtocolConfig {
     /**
      * Protocol configuration type.
diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java
index b06301dbbdc..d1fe59ed0c0 100644
--- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java
+++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java
@@ -22,13 +22,14 @@
 import io.helidon.builder.api.Prototype;
 import io.helidon.http.RequestedUriDiscoveryContext;
 import io.helidon.webserver.spi.ProtocolConfig;
+import io.helidon.webserver.spi.ProtocolConfigProvider;
 
 /**
  * HTTP/2 server configuration.
  */
 @Prototype.Blueprint(decorator = Http2ConfigBlueprint.Http2ConfigDecorator.class)
-@Prototype.Configured
-@Prototype.Provides(ProtocolConfig.class)
+@Prototype.Configured(root = false, value = Http2ConnectionProvider.CONFIG_NAME)
+@Prototype.Provides(ProtocolConfigProvider.class)
 interface Http2ConfigBlueprint extends ProtocolConfig {
     /**
      * The size of the largest frame payload that the sender is willing to receive in bytes.
diff --git a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java
index 4c1f15c3bf1..32bf09fba46 100644
--- a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java
+++ b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -24,7 +24,7 @@
 import io.helidon.webserver.observe.spi.ObserveProvider;
 
 @Prototype.Blueprint
-@Prototype.Configured
+@Prototype.Configured(root = false, value = "config")
 @Prototype.Provides(ObserveProvider.class)
 interface ConfigObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory {
     @Option.Configured
diff --git a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java
index f6f8a2f4e70..7f66920251d 100644
--- a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java
+++ b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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,7 +27,7 @@
  * Info Observer configuration.
  */
 @Prototype.Blueprint
-@Prototype.Configured
+@Prototype.Configured(root = false, value = "info")
 @Prototype.Provides(ObserveProvider.class)
 interface InfoObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory {
     @Option.Configured
diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java
index b91a384eab1..fbb689e849d 100644
--- a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java
+++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -25,7 +25,7 @@
  * Log Observer configuration.
  */
 @Prototype.Blueprint
-@Prototype.Configured
+@Prototype.Configured(root = false, value = "log")
 @Prototype.Provides(ObserveProvider.class)
 interface LogObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory {
     @Option.Configured
diff --git a/webserver/observe/tracing/pom.xml b/webserver/observe/tracing/pom.xml
index 355ea4a973e..283e5266bce 100644
--- a/webserver/observe/tracing/pom.xml
+++ b/webserver/observe/tracing/pom.xml
@@ -50,6 +50,11 @@
             helidon-common-features-api
             true
         
+        
+            io.helidon.config
+            helidon-config-metadata
+            true
+        
         
             io.helidon.webserver.testing.junit5
             helidon-webserver-testing-junit5
diff --git a/webserver/observe/tracing/src/main/java/io/helidon/webserver/observe/tracing/TracingObserverConfigBlueprint.java b/webserver/observe/tracing/src/main/java/io/helidon/webserver/observe/tracing/TracingObserverConfigBlueprint.java
index b2f1c5cef64..b7d3a3ba084 100644
--- a/webserver/observe/tracing/src/main/java/io/helidon/webserver/observe/tracing/TracingObserverConfigBlueprint.java
+++ b/webserver/observe/tracing/src/main/java/io/helidon/webserver/observe/tracing/TracingObserverConfigBlueprint.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -33,7 +33,7 @@
  * @see io.helidon.webserver.observe.tracing.TracingObserver#builder()
  */
 @Prototype.Blueprint(decorator = TracingObserverSupport.TracingObserverDecorator.class)
-@Prototype.Configured
+@Prototype.Configured(root = false, value = "tracing")
 @Prototype.Provides(ObserveProvider.class)
 interface TracingObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory {
     @Option.Default("tracing")
@@ -60,6 +60,7 @@ interface TracingObserverConfigBlueprint extends ObserverConfigBase, Prototype.F
      *
      * By default we disable both the SE-style paths ({@code /observe/health}) and the MP-style paths ({@code /health}).
      */
+
     /**
      * Path specific configuration of tracing.
      *
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
index 93f38aa76bb..f7673a98a2a 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
@@ -22,13 +22,14 @@
 import io.helidon.builder.api.Prototype;
 import io.helidon.http.RequestedUriDiscoveryContext;
 import io.helidon.webserver.spi.ProtocolConfig;
+import io.helidon.webserver.spi.ProtocolConfigProvider;
 
 /**
  * HTTP/1.1 server configuration.
  */
 @Prototype.Blueprint(decorator = Http1BuilderDecorator.class)
-@Prototype.Configured
-@Prototype.Provides(ProtocolConfig.class)
+@Prototype.Configured(root = false, value = Http1ConnectionProvider.CONFIG_NAME)
+@Prototype.Provides(ProtocolConfigProvider.class)
 interface Http1ConfigBlueprint extends ProtocolConfig {
     /**
      * Name of this configuration, in most cases the same as {@link #type()}.
diff --git a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java
index a354775fc37..29594b146e4 100644
--- a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java
+++ b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java
@@ -21,13 +21,14 @@
 import io.helidon.builder.api.Option;
 import io.helidon.builder.api.Prototype;
 import io.helidon.webserver.spi.ProtocolConfig;
+import io.helidon.webserver.spi.ProtocolConfigProvider;
 
 /**
  * WebSocket protocol configuration.
  */
 @Prototype.Blueprint
-@Prototype.Configured
-@Prototype.Provides(ProtocolConfig.class)
+@Prototype.Configured(root = false, value = WsUpgradeProvider.CONFIG_NAME)
+@Prototype.Provides(ProtocolConfigProvider.class)
 interface WsConfigBlueprint extends ProtocolConfig {
     /**
      * WebSocket origins.

From 5343142f54a1ccaa7d19c0dfebeccadd35e800a6 Mon Sep 17 00:00:00 2001
From: Tomas Langer 
Date: Tue, 18 Jun 2024 11:46:21 +0200
Subject: [PATCH 005/190] 4.x: Improved parsing of HTTP/1 prologue and headers.
 (#8890)

Using SWAR (SIMD within a register) batch read technique to optimize lookup when parsing HTTP/1.1 prologue and headers.

This technique allows searching the bytes by long words (8 bytes per cycle), instead of one by one. This improves speed, as it eliminates some bound checks, for cycles, and branches from the code.
We may get additional improvement in the future by using vectors for SIMD parallelization (now an incubator feature in JDK).
---
 .../java/io/helidon/common/buffers/Bytes.java | 157 ++++++++++++++-
 .../io/helidon/common/buffers/DataReader.java |  85 +++++++-
 .../io/helidon/common/buffers/LazyString.java |  21 +-
 .../java/io/helidon/http/HeadersImpl.java     |   4 +-
 .../io/helidon/http/Http1HeadersParser.java   |  75 ++++++-
 .../webserver/benchmark/jmh/HuJmhTest.java    |  59 ------
 .../benchmark/jmh/ParsingJmhTest.java         |  75 +++++++
 .../webserver/http1/Http1Prologue.java        | 188 ++++++++++++------
 8 files changed, 516 insertions(+), 148 deletions(-)
 delete mode 100644 tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/HuJmhTest.java
 create mode 100644 tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/ParsingJmhTest.java

diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java b/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java
index f6ec7a596d1..b7153718d9a 100644
--- a/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java
+++ b/common/buffers/src/main/java/io/helidon/common/buffers/Bytes.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 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.
@@ -16,6 +16,16 @@
 
 package io.helidon.common.buffers;
 
+import java.nio.ByteOrder;
+
+/*
+ * SWAR related methods in this class are heavily inspired by:
+ * PR https://github.com/netty/netty/pull/10737
+ *
+ * From Netty
+ * Distributed under Apache License, Version 2.0
+ */
+
 /**
  * Bytes commonly used in HTTP.
  */
@@ -69,6 +79,151 @@ public final class Bytes {
      */
     public static final byte TAB_BYTE = (byte) '\t';
 
+    private static final boolean BYTE_ORDER_LE = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
+
     private Bytes() {
     }
+
+    /**
+     * This is using a SWAR (SIMD Within A Register) batch read technique to minimize bound-checks and improve memory
+     * usage while searching for {@code value}.
+     * 

+ * This method does NOT do a bound check on the buffer length and the fromIndex and toIndex, neither does it check + * if the {@code toIndex} is bigger than the {@code fromIndex}. + * + * @param buffer the byte buffer to search + * @param fromIndex first index in the array + * @param toIndex last index in the array + * @param value to search for + * @return first index of the desired byte {@code value}, or {@code -1} if not found + */ + public static int firstIndexOf(byte[] buffer, int fromIndex, int toIndex, byte value) { + if (fromIndex == toIndex || buffer.length == 0) { + // fast path for empty buffers, or empty range + return -1; + } + int length = toIndex - fromIndex; + int offset = fromIndex; + int byteCount = length & 7; + if (byteCount > 0) { + int index = unrolledFirstIndexOf(buffer, fromIndex, byteCount, value); + if (index != -1) { + return index; + } + offset += byteCount; + if (offset == toIndex) { + return -1; + } + } + int longCount = length >>> 3; + long pattern = compilePattern(value); + for (int i = 0; i < longCount; i++) { + // use the faster available getLong + long word = toWord(buffer, offset); + int index = firstInstance(word, pattern); + if (index < Long.BYTES) { + return offset + index; + } + offset += Long.BYTES; + } + return -1; + } + + /** + * Converts the first 8 bytes from {@code offset} to a long, using appropriate byte order for this machine. + *

    + *
  • This method DOES NOT do a bound check
  • + *
  • This method DOES NOT validate there are 8 bytes available
  • + *
+ * + * @param buffer bytes to convert + * @param offset offset within the byte array + * @return long word from the first 8 bytes from offset + */ + public static long toWord(byte[] buffer, int offset) { + return BYTE_ORDER_LE ? toWordLe(buffer, offset) : toWordBe(buffer, offset); + } + + // create a pattern for a byte, so we can search for it in a whole word + private static long compilePattern(byte byteToFind) { + return (byteToFind & 0xFFL) * 0x101010101010101L; + } + + // first instance of a pattern within a word (see above compilePattern) + private static int firstInstance(long word, long pattern) { + long input = word ^ pattern; + long tmp = (input & 0x7F7F7F7F7F7F7F7FL) + 0x7F7F7F7F7F7F7F7FL; + tmp = ~(tmp | input | 0x7F7F7F7F7F7F7F7FL); + int binaryPosition = Long.numberOfTrailingZeros(tmp); + return binaryPosition >>> 3; + } + + // to a little endian word + private static long toWordLe(byte[] buffer, int index) { + return (long) buffer[index] & 0xff + | ((long) buffer[index + 1] & 0xff) << 8 + | ((long) buffer[index + 2] & 0xff) << 16 + | ((long) buffer[index + 3] & 0xff) << 24 + | ((long) buffer[index + 4] & 0xff) << 32 + | ((long) buffer[index + 5] & 0xff) << 40 + | ((long) buffer[index + 6] & 0xff) << 48 + | ((long) buffer[index + 7] & 0xff) << 56; + } + + private static long toWordBe(byte[] buffer, int index) { + return ((long) buffer[index] & 0xff) << 56 + | ((long) buffer[index + 1] & 0xff) << 48 + | ((long) buffer[index + 2] & 0xff) << 40 + | ((long) buffer[index + 3] & 0xff) << 32 + | ((long) buffer[index + 4] & 0xff) << 24 + | ((long) buffer[index + 5] & 0xff) << 16 + | ((long) buffer[index + 6] & 0xff) << 8 + | (long) buffer[index + 7] & 0xff; + } + + // this method is copied from Netty, and validated by them that it is the optimal + // way to figure out the index, see https://github.com/netty/netty/issues/10731 + private static int unrolledFirstIndexOf(byte[] buffer, int fromIndex, int byteCount, byte value) { + assert byteCount > 0 && byteCount < 8; + if (buffer[fromIndex] == value) { + return fromIndex; + } + if (byteCount == 1) { + return -1; + } + if (buffer[fromIndex + 1] == value) { + return fromIndex + 1; + } + if (byteCount == 2) { + return -1; + } + if (buffer[fromIndex + 2] == value) { + return fromIndex + 2; + } + if (byteCount == 3) { + return -1; + } + if (buffer[fromIndex + 3] == value) { + return fromIndex + 3; + } + if (byteCount == 4) { + return -1; + } + if (buffer[fromIndex + 4] == value) { + return fromIndex + 4; + } + if (byteCount == 5) { + return -1; + } + if (buffer[fromIndex + 5] == value) { + return fromIndex + 5; + } + if (byteCount == 6) { + return -1; + } + if (buffer[fromIndex + 6] == value) { + return fromIndex + 6; + } + return -1; + } } diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java index 0777387ceb8..f2402fb75c9 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java @@ -289,6 +289,36 @@ public String readAsciiString(int len) { } } + /** + * Read byte array. + * + * @param len number of bytes of the string + * @return string value + */ + public byte[] readBytes(int len) { + ensureAvailable(); // we have at least 1 byte + byte[] b = new byte[len]; + + if (len <= head.available()) { // fast case + System.arraycopy(head.bytes, head.position, b, 0, len); + head.position += len; + return b; + } else { + int remaining = len; + for (Node n = head; remaining > 0; n = n.next) { + ensureAvailable(); + int toAdd = Math.min(remaining, n.available()); + System.arraycopy(n.bytes, n.position, b, len - remaining, toAdd); + remaining -= toAdd; + n.position += toAdd; + if (remaining > 0 && n.next == null) { + pullData(); + } + } + return b; + } + } + /** * Read an ascii string until new line. * @@ -367,31 +397,64 @@ public int findNewLine(int max) throws IncorrectNewLineException { ensureAvailable(); int idx = 0; Node n = head; + int indexWithinNode = n.position; + while (true) { byte[] barr = n.bytes; - for (int i = n.position; i < barr.length && idx < max; i++, idx++) { - if (barr[i] == Bytes.LF_BYTE && !ignoreLoneEol) { - throw new IncorrectNewLineException("Found LF (" + idx + ") without preceding CR. :\n" + this.debugDataHex()); - } else if (barr[i] == Bytes.CR_BYTE) { - byte nextByte; - if (i + 1 < barr.length) { - nextByte = barr[i + 1]; - } else { - nextByte = n.next().peek(); + int maxLength = Math.min(max - idx, barr.length - indexWithinNode); + int crIndex = Bytes.firstIndexOf(barr, indexWithinNode, indexWithinNode + maxLength, Bytes.CR_BYTE); + + if (crIndex == -1) { + int lfIndex = Bytes.firstIndexOf(barr, indexWithinNode, indexWithinNode + maxLength, Bytes.LF_BYTE); + if (lfIndex != -1) { + if (!ignoreLoneEol) { + throw new IncorrectNewLineException("Found LF (" + (idx + lfIndex - n.position) + + ") without preceding CR. :\n" + this.debugDataHex()); } + } + // not found, continue with next buffer + idx += maxLength; + if (idx >= max) { + // not found and reached the limit + return max; + } + n = n.next(); + indexWithinNode = n.position; + continue; + } else { + // found, next byte should be LF + if (crIndex == barr.length - 1) { + // found CR as the last byte of the current node, peek next node + byte nextByte = n.next().peek(); if (nextByte == Bytes.LF_BYTE) { - return idx; + return idx + crIndex - n.position; + } + if (!ignoreLoneEol) { + throw new IncorrectNewLineException("Found CR (" + (idx + crIndex - n.position) + + ") without following LF. :\n" + this.debugDataHex()); + } + } else { + // found CR within the current array + byte nextByte = barr[crIndex + 1]; + if (nextByte == Bytes.LF_BYTE) { + return idx + crIndex - n.position; } if (!ignoreLoneEol) { throw new IncorrectNewLineException("Found CR (" + idx + ") without following LF. :\n" + this.debugDataHex()); } + indexWithinNode = crIndex + 1; + idx += indexWithinNode; + continue; } } - if (idx == max) { + + idx += maxLength; + if (idx >= max) { return max; } n = n.next(); + indexWithinNode = n.position; } } diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java b/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java index 4843e6bac84..3e5ea5e928b 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/LazyString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -22,6 +22,12 @@ * String that materializes only when requested. */ public class LazyString { + private static final boolean[] IS_OWS = new boolean[256]; + static { + IS_OWS[Bytes.SPACE_BYTE] = true; + IS_OWS[Bytes.TAB_BYTE] = true; + } + private final byte[] buffer; private final int offset; private final int length; @@ -69,9 +75,9 @@ public String stripOws() { int newOffset = offset; int newLength = length; for (int i = offset; i < offset + length; i++) { - if (isOws(buffer[i])) { + if (IS_OWS[buffer[i] & 0xff]) { newOffset = i + 1; - newLength = length - (newOffset - offset); + newLength--; } else { // no more white space, go from the end now break; @@ -79,7 +85,7 @@ public String stripOws() { } // now we need to go from the end of the string for (int i = offset + length - 1; i > newOffset; i--) { - if (isOws(buffer[i])) { + if (IS_OWS[buffer[i] & 0xff]) { newLength--; } else { break; @@ -99,11 +105,4 @@ public String toString() { } return stringValue; } - - private boolean isOws(byte aByte) { - return switch (aByte) { - case Bytes.SPACE_BYTE, Bytes.TAB_BYTE -> true; - default -> false; - }; - } } diff --git a/http/http/src/main/java/io/helidon/http/HeadersImpl.java b/http/http/src/main/java/io/helidon/http/HeadersImpl.java index f11c6de17a6..cc730b62fc0 100644 --- a/http/http/src/main/java/io/helidon/http/HeadersImpl.java +++ b/http/http/src/main/java/io/helidon/http/HeadersImpl.java @@ -162,7 +162,9 @@ public T set(Header header) { HeaderName name = header.headerName(); Header usedHeader = header; - if (header instanceof HeaderWriteable) { + if (header instanceof HeaderValueLazy) { + // use it directly (lazy values are write once) + } else if (header instanceof HeaderWriteable) { // we must create a new instance, as we risk modifying state of the provided header usedHeader = new HeaderValueCopy(header); } diff --git a/http/http/src/main/java/io/helidon/http/Http1HeadersParser.java b/http/http/src/main/java/io/helidon/http/Http1HeadersParser.java index 4a7ebb1878b..4620b4a838b 100644 --- a/http/http/src/main/java/io/helidon/http/Http1HeadersParser.java +++ b/http/http/src/main/java/io/helidon/http/Http1HeadersParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -26,14 +26,29 @@ * Used by both HTTP server and client to parse headers from {@link io.helidon.common.buffers.DataReader}. */ public final class Http1HeadersParser { - // TODO expand set of fastpath headers private static final byte[] HD_HOST = (HeaderNameEnum.HOST.defaultCase() + ":").getBytes(StandardCharsets.UTF_8); private static final byte[] HD_ACCEPT = (HeaderNameEnum.ACCEPT.defaultCase() + ":").getBytes(StandardCharsets.UTF_8); private static final byte[] HD_CONNECTION = (HeaderNameEnum.CONNECTION.defaultCase() + ":").getBytes(StandardCharsets.UTF_8); - private static final byte[] HD_USER_AGENT = (HeaderNameEnum.USER_AGENT.defaultCase() + ":").getBytes(StandardCharsets.UTF_8); + private static final long CLOSE_WORD = ' ' + | 'c' << 8 + | 'l' << 16 + | 'o' << 24 + | (long) 's' << 32 + | (long) 'e' << 40; + private static final long KEEP_ALIVE_WORD_1 = ' ' + | 'k' << 8 + | 'e' << 16 + | 'e' << 24 + | (long) 'p' << 32 + | (long) '-' << 40 + | (long) 'a' << 48 + | (long) 'l' << 56; + private static final long KEEP_ALIVE_WORD_2 = 'i' + | 'v' << 8 + | 'e' << 16; private Http1HeadersParser() { } @@ -56,18 +71,24 @@ public static WritableHeaders readHeaders(DataReader reader, int maxHeadersSi return headers; } - HeaderName header = readHeaderName(reader, maxLength, validate); + HeaderName header = readHeaderName(reader, maxLength); maxLength -= header.defaultCase().length() + 2; int eol = reader.findNewLine(maxLength); if (eol == maxLength) { throw new IllegalStateException("Header size exceeded"); } - // we do not need the string until somebody asks for this header (unless validation is on) - LazyString value = reader.readLazyString(StandardCharsets.US_ASCII, eol); + // optimization for Connection, as we always need to analyze it + Header headerValue; + if (header.equals(HeaderNames.CONNECTION)) { + headerValue = connectionHeaderValue(reader, eol); + } else { + // we do not need the string until somebody asks for this header (unless validation is on) + LazyString value = reader.readLazyString(StandardCharsets.US_ASCII, eol); + headerValue = HeaderValues.create(header, value); + } reader.skip(2); maxLength -= eol + 1; - Header headerValue = HeaderValues.create(header, value); headers.add(headerValue); if (validate) { headerValue.validate(); @@ -78,9 +99,45 @@ public static WritableHeaders readHeaders(DataReader reader, int maxHeadersSi } } + private static Header connectionHeaderValue(DataReader reader, int eol) { + byte[] bytes = reader.readBytes(eol); + + // space and `keep-alive` + if (bytes.length == 11) { + if (isKeepAlive(bytes)) { + return HeaderValues.CONNECTION_KEEP_ALIVE; + } + } + // space and `close` + if (bytes.length == 6 && isClose(bytes)) { + return HeaderValues.CONNECTION_CLOSE; + } + // some other (unexpected) combination + return HeaderValues.create(HeaderNames.CONNECTION, new LazyString(bytes, 0, bytes.length, StandardCharsets.US_ASCII)); + } + + private static boolean isKeepAlive(byte[] buffer) { + if (Bytes.toWord(buffer, 0) != KEEP_ALIVE_WORD_1) { + return false; + } + long endWord = buffer[8] & 0xff + | (buffer[9] & 0xff) << 8 + | (buffer[10] & 0xff) << 16; + return endWord == KEEP_ALIVE_WORD_2; + } + + private static boolean isClose(byte[] buffer) { + long word = buffer[0] & 0xff + | (buffer[1] & 0xff) << 8 + | (buffer[2] & 0xff) << 16 + | ((long) buffer[3] & 0xff) << 24 + | ((long) buffer[4] & 0xff) << 32 + | ((long) buffer[5] & 0xff) << 40; + return word == CLOSE_WORD; + } + private static HeaderName readHeaderName(DataReader reader, - int maxLength, - boolean validate) { + int maxLength) { switch (reader.lookup()) { case (byte) 'H' -> { if (reader.startsWith(HD_HOST)) { diff --git a/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/HuJmhTest.java b/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/HuJmhTest.java deleted file mode 100644 index 2e518132545..00000000000 --- a/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/HuJmhTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2022, 2023 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.benchmark.jmh; - -import java.util.Base64; -import java.util.Random; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.infra.Blackhole; - -@State(Scope.Benchmark) -public class HuJmhTest { - private String strOne; - private String strTwo; - - @Setup - public void setup() { - byte[] rand = new byte[26]; - new Random().nextBytes(rand); - String str = Base64.getEncoder().encodeToString(rand); - strOne = "/" + str; - strTwo = str; - } - - @Benchmark - public void startsWith(Blackhole bh) { - boolean one = strOne.startsWith("/"); - boolean two = strTwo.startsWith("/"); - - bh.consume(one); - bh.consume(two); - } - - @Benchmark - public void charAt(Blackhole bh) { - boolean one = strOne.charAt(0) == '/'; - boolean two = strTwo.charAt(0) == '/'; - - bh.consume(one); - bh.consume(two); - } -} diff --git a/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/ParsingJmhTest.java b/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/ParsingJmhTest.java new file mode 100644 index 00000000000..857743d38c0 --- /dev/null +++ b/tests/benchmark/jmh/src/main/java/io/helidon/webserver/benchmark/jmh/ParsingJmhTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, 2024 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.benchmark.jmh; + +import java.nio.charset.StandardCharsets; + +import io.helidon.common.buffers.DataReader; +import io.helidon.http.HttpPrologue; +import io.helidon.http.Method; +import io.helidon.http.WritableHeaders; +import io.helidon.webserver.http1.Http1Headers; +import io.helidon.webserver.http1.Http1Prologue; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +/* +These numbers are irrelevant on a different machine. This is just a not on improvements done by SWAR used in parsing. +Prologue +original 5633378.812 ± 261834.000 ops/s +new 6842537.897 ± 109193.680 ops/s + +Headers +original 2930640.115 ± 39855.689 ops/s +new 5342407.893 ops/s + */ +@State(Scope.Benchmark) +public class ParsingJmhTest { + private static final byte[] PROLOGUE_WITH_EOL = "POST /some/path?query=some_query_message_text HTTP/1.1\r\n" + .getBytes(StandardCharsets.US_ASCII); + private static final HttpPrologue PROLOGUE = HttpPrologue.create("HTTP", "HTTP", "1.1", Method.GET, "/", false); + private static final byte[] HEADERS = """ + Host: localhost:8080\r + User-Agent: curl/7.68.0\r + Connection: keep-alive\r + Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r + \r + """.getBytes(StandardCharsets.US_ASCII); + + @Benchmark + public void prologue(Blackhole bh) { + DataReader reader = new DataReader(() -> PROLOGUE_WITH_EOL); + Http1Prologue p = new Http1Prologue(reader, 1024, false); + HttpPrologue httpPrologue = p.readPrologue(); + bh.consume(httpPrologue.method()); + bh.consume(httpPrologue.query()); + bh.consume(httpPrologue.uriPath()); + bh.consume(httpPrologue); + } + + @Benchmark + public void headers(Blackhole bh) { + DataReader reader = new DataReader(() -> HEADERS); + Http1Headers p = new Http1Headers(reader, 1024, false); + WritableHeaders headers = p.readHeaders(PROLOGUE); + + bh.consume(headers); + } +} 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 8ad76e03c1d..29d53e44c54 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, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -24,7 +24,6 @@ import io.helidon.http.HttpPrologue; import io.helidon.http.Method; import io.helidon.http.RequestException; -import io.helidon.http.Status; import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.http.DirectTransportRequest; @@ -32,7 +31,37 @@ * HTTP 1 prologue parsing support. */ public final class Http1Prologue { - private static final byte[] GET_BYTES = "GET ".getBytes(StandardCharsets.UTF_8); // space included + /* + The string HTTP/1.1 (as used in protocol version in prologue's third section) + */ + private static final long HTTP_1_1_LONG = 'H' + | 'T' << 8 + | 'T' << 16 + | 'P' << 24 + | (long) '/' << 32 + | (long) '1' << 40 + | (long) '.' << 48 + | (long) '1' << 56; + /* + GET string as int + */ + private static final int GET_INT = 'G' + | 'E' << 8 + | 'T' << 16; + /* + PUT string as int + */ + private static final int PUT_INT = 'P' + | 'U' << 8 + | 'T' << 16; + /* + POST string as int + */ + private static final int POST_INT = 'P' + | 'O' << 8 + | 'S' << 16 + | 'T' << 24; + private static final String HTTP_1_1 = "HTTP/1.1"; private final DataReader reader; private final int maxLength; @@ -79,63 +108,48 @@ private static RequestException badRequest(String message, String method, String .build(); } - private static Method readMethod(DataReader reader, int maxLen) { - if (reader.startsWith(GET_BYTES)) { - reader.skip(GET_BYTES.length); - return Method.GET; - } - int firstSpace = reader.findOrNewLine(Bytes.SPACE_BYTE, maxLen); - if (firstSpace < 0) { - throw badRequest("Invalid prologue, missing space " + reader.debugDataHex(), "", "", "", ""); - } else if (firstSpace == maxLen) { - throw badRequest("Prologue size exceeded", "", "", "", ""); + private static Method readMethod(byte[] bytes, int index, int spaceIndex) { + int len = spaceIndex - index; + if (len == 3) { + if (isGetMethod(bytes, index)) { + return Method.GET; + } + if (isPutMethod(bytes, index)) { + return Method.PUT; + } + } else if (len == 4 && isPostMethod(bytes, index)) { + return Method.POST; } - Method method = Method.create(reader.readAsciiString(firstSpace)); - reader.skip(1); - return method; + return Method.create(new String(bytes, index, len, StandardCharsets.US_ASCII)); + } + + private static boolean isGetMethod(byte[] bytes, int index) { + int maybeGet = bytes[index] & 0xff + | (bytes[index + 1] & 0xff) << 8 + | (bytes[index + 2] & 0xff) << 16; + return maybeGet == GET_INT; + } + + private static boolean isPutMethod(byte[] bytes, int index) { + int maybeGet = bytes[index] & 0xff + | (bytes[index + 1] & 0xff) << 8 + | (bytes[index + 2] & 0xff) << 16; + return maybeGet == PUT_INT; + } + + private static boolean isPostMethod(byte[] bytes, int index) { + int maybePost = bytes[index] & 0xff + | (bytes[index + 1] & 0xff) << 8 + | (bytes[index + 2] & 0xff) << 16 + | (bytes[index + 3] & 0xff) << 24; + return maybePost == POST_INT; } private HttpPrologue doRead() { - // > GET /loom/slow HTTP/1.1 - String protocol; - String path; - Method method; + int eol; + try { - int maxLen = maxLength; - try { - method = readMethod(reader, maxLen); - } catch (IllegalArgumentException e) { - // we need to validate method contains only allowed characters - throw badRequest("Invalid prologue, method not valid (" + e.getMessage() + ")", "", "", "", ""); - } - maxLen -= method.length(); // length of method - maxLen--; // space - - int secondSpace = reader.findOrNewLine(Bytes.SPACE_BYTE, maxLen); - if (secondSpace < 0) { - throw badRequest("Invalid prologue" + reader.debugDataHex(), method.text(), "", "", ""); - } else if (secondSpace == maxLen) { - throw RequestException.builder() - .message("Request URI too long.") - .type(DirectHandler.EventType.BAD_REQUEST) - .status(Status.REQUEST_URI_TOO_LONG_414) - .request(DirectTransportRequest.create("", method.text(), reader.readAsciiString(secondSpace))) - .build(); - } - path = reader.readAsciiString(secondSpace); - reader.skip(1); - maxLen -= secondSpace; - maxLen--; // space - - int eol = reader.findNewLine(maxLen); - if (eol == maxLen) { - throw badRequest("Prologue size exceeded", method.text(), path, "", ""); - } - if (path.isBlank()) { - throw badRequest("Path can't be empty", method.text(), path, "", ""); - } - protocol = reader.readAsciiString(eol); - reader.skip(2); // \r\n + eol = reader.findNewLine(maxLength); } catch (DataReader.IncorrectNewLineException e) { throw RequestException.builder() .message("Invalid prologue: " + e.getMessage()) @@ -143,8 +157,53 @@ private HttpPrologue doRead() { .cause(e) .build(); } + if (eol == maxLength) { + // exceeded maximal length, we do not want to parse it anyway + throw RequestException.builder() + .message("Prologue size exceeded") + .type(DirectHandler.EventType.BAD_REQUEST) + .build(); + } + + byte[] prologueBytes = reader.readBytes(eol); + reader.skip(2); // \r\n + + // > GET /loom/slow HTTP/1.1 + Method method; + String path; + String protocol; + + /* + Read HTTP Method + */ + int currentIndex = 0; + int nextSpace = nextSpace(prologueBytes, currentIndex); + if (nextSpace == -1) { + throw badRequest("Invalid prologue, missing space " + reader.debugDataHex(), "", "", "", ""); + } + method = readMethod(prologueBytes, currentIndex, nextSpace); + currentIndex = nextSpace + 1; // continue after the space - if (!"HTTP/1.1".equals(protocol)) { + /* + Read HTTP Path + */ + nextSpace = nextSpace(prologueBytes, currentIndex); + if (nextSpace == -1) { + throw badRequest("Invalid prologue, missing space " + reader.debugDataHex(), method.text(), "", "", ""); + } + path = new String(prologueBytes, currentIndex, (nextSpace - currentIndex), StandardCharsets.US_ASCII); + currentIndex = nextSpace + 1; // continue after the space + if (path.isBlank()) { + throw badRequest("Path can't be empty", method.text(), path, "", ""); + } + + /* + Read HTTP Version (we only support HTTP/1.1 + */ + protocol = readProtocol(prologueBytes, currentIndex); + // we always use the same constant + //noinspection StringEquality + if (protocol != HTTP_1_1) { throw badRequest("Invalid protocol and/or version", method.text(), path, protocol, ""); } @@ -159,4 +218,21 @@ private HttpPrologue doRead() { throw badRequest("Invalid path: " + e.getMessage(), method.text(), path, "HTTP", "1.1"); } } + + private int nextSpace(byte[] prologueBytes, int currentIndex) { + return Bytes.firstIndexOf(prologueBytes, currentIndex, prologueBytes.length - 1, Bytes.SPACE_BYTE); + } + + private String readProtocol(byte[] bytes, int index) { + int length = bytes.length - index; + + if (length == 8) { + long word = Bytes.toWord(bytes, index); + if (word == HTTP_1_1_LONG) { + return HTTP_1_1; + } + } + + return new String(bytes, StandardCharsets.US_ASCII); + } } From b13a31c82af0f2635dd0bfbb727af95b5b33f90d Mon Sep 17 00:00:00 2001 From: Joe DiPol Date: Thu, 20 Jun 2024 14:26:47 -0700 Subject: [PATCH 006/190] Bump oci-sdk to 3.43.2 (#8899) --- dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/pom.xml b/dependencies/pom.xml index c4b41226b91..00574eab23d 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -126,7 +126,7 @@ 7.0.0.Final 5.12.0 4.1.108.Final - 3.39.0 + 3.43.2 21 + + 4.0.0 + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-project + 4.1.0-SNAPSHOT + + helidon-integrations-oci-authentication-instance + Helidon Integrations OCI Authentication Instance + + + Authentication based on Instance's principal + + + + + io.helidon.integrations.oci + helidon-integrations-oci + + + io.helidon.service + helidon-service-registry + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-jdk14 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/AuthenticationMethodInstancePrincipal.java b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/AuthenticationMethodInstancePrincipal.java new file mode 100644 index 00000000000..0c262c9750d --- /dev/null +++ b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/AuthenticationMethodInstancePrincipal.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 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.integrations.oci.authentication.instance; + +import java.util.Optional; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.integrations.oci.HelidonOci; +import io.helidon.integrations.oci.OciConfig; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider.InstancePrincipalsAuthenticationDetailsProviderBuilder; + +/** + * Instance principal authentication method, uses the + * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 40) +@Service.Provider +class AuthenticationMethodInstancePrincipal implements OciAuthenticationMethod { + private static final System.Logger LOGGER = System.getLogger(AuthenticationMethodInstancePrincipal.class.getName()); + private static final String METHOD = "instance-principal"; + + private final LazyValue> provider; + + AuthenticationMethodInstancePrincipal(OciConfig config, + InstancePrincipalsAuthenticationDetailsProviderBuilder builder) { + provider = createProvider(config, builder); + } + + @Override + public String method() { + return METHOD; + } + + @Override + public Optional provider() { + return provider.get(); + } + + private static LazyValue> + createProvider(OciConfig config, + InstancePrincipalsAuthenticationDetailsProviderBuilder builder) { + return LazyValue.create(() -> { + if (HelidonOci.imdsAvailable(config)) { + return Optional.of(builder.build()); + } + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, "OCI Metadata service is not available, " + + "instance principal cannot be used."); + } + return Optional.empty(); + }); + } + +} diff --git a/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/InstancePrincipalBuilderProvider.java b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/InstancePrincipalBuilderProvider.java new file mode 100644 index 00000000000..769715317de --- /dev/null +++ b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/InstancePrincipalBuilderProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 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.integrations.oci.authentication.instance; + +import java.net.URI; +import java.util.function.Supplier; + +import io.helidon.integrations.oci.OciConfig; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider.InstancePrincipalsAuthenticationDetailsProviderBuilder; + +/** + * Instance principal builder provider, uses the + * {@link InstancePrincipalsAuthenticationDetailsProviderBuilder}. + */ +@Service.Provider +class InstancePrincipalBuilderProvider implements Supplier { + private final OciConfig config; + + InstancePrincipalBuilderProvider(OciConfig config) { + this.config = config; + } + + @Override + public InstancePrincipalsAuthenticationDetailsProviderBuilder get() { + var builder = InstancePrincipalsAuthenticationDetailsProvider.builder() + .timeoutForEachRetry((int) config.authenticationTimeout().toMillis()); + + config.imdsBaseUri() + .map(URI::toString) + .ifPresent(builder::metadataBaseUrl); + + return builder; + } + +} diff --git a/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/package-info.java b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/package-info.java new file mode 100644 index 00000000000..f121134bd6b --- /dev/null +++ b/integrations/oci/authentication/instance/src/main/java/io/helidon/integrations/oci/authentication/instance/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on instance principal. + */ +package io.helidon.integrations.oci.authentication.instance; diff --git a/integrations/oci/authentication/instance/src/main/java/module-info.java b/integrations/oci/authentication/instance/src/main/java/module-info.java new file mode 100644 index 00000000000..ea1ab72d12c --- /dev/null +++ b/integrations/oci/authentication/instance/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on instance principal. + */ +module io.helidon.integrations.oci.authentication.instance { + requires io.helidon.common; + requires io.helidon.service.registry; + requires io.helidon.integrations.oci; + + requires oci.java.sdk.common; + + exports io.helidon.integrations.oci.authentication.instance; +} diff --git a/integrations/oci/authentication/oke-workload/pom.xml b/integrations/oci/authentication/oke-workload/pom.xml new file mode 100644 index 00000000000..5fc00f970b6 --- /dev/null +++ b/integrations/oci/authentication/oke-workload/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-project + 4.1.0-SNAPSHOT + + helidon-integrations-oci-authentication-oke-workload + Helidon Integrations OCI Authentication Workload + + + Authentication based on Workload identity + + + + + io.helidon.integrations.oci + helidon-integrations-oci + + + io.helidon.service + helidon-service-registry + + + com.oracle.oci.sdk + oci-java-sdk-addons-oke-workload-identity + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-jdk14 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/AuthenticationMethodOkeWorkload.java b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/AuthenticationMethodOkeWorkload.java new file mode 100644 index 00000000000..22405816ef7 --- /dev/null +++ b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/AuthenticationMethodOkeWorkload.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 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.integrations.oci.authentication.okeworkload; + +import java.lang.System.Logger.Level; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.integrations.oci.OciConfig; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider; + +/** + * Instance principal authentication method, uses the + * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 40) +@Service.Provider +class AuthenticationMethodOkeWorkload implements OciAuthenticationMethod { + private static final System.Logger LOGGER = System.getLogger(AuthenticationMethodOkeWorkload.class.getName()); + + private static final String METHOD = "oke-workload-identity"; + + /* + These constants are copied from + com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider + as they are not public + */ + private static final String SERVICE_ACCOUNT_CERT_PATH_DEFAULT = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + private static final String SERVICE_ACCOUNT_CERT_PATH_ENV = "OCI_KUBERNETES_SERVICE_ACCOUNT_CERT_PATH"; + + private final LazyValue> provider; + + AuthenticationMethodOkeWorkload(OciConfig config) { + provider = createProvider(config); + } + + @Override + public String method() { + return METHOD; + } + + @Override + public Optional provider() { + return provider.get(); + } + + private static LazyValue> createProvider(OciConfig config) { + return LazyValue.create(() -> { + if (available()) { + return Optional.of(OkeWorkloadIdentityAuthenticationDetailsProvider.builder() + .build()); + + } + return Optional.empty(); + }); + } + + /** + * Returns true if the configured (or default) path with OKE account certificate path exists and is a + * regular file. + */ + private static boolean available() { + String usedPath = System.getenv(SERVICE_ACCOUNT_CERT_PATH_ENV); + usedPath = usedPath == null ? SERVICE_ACCOUNT_CERT_PATH_DEFAULT : usedPath; + + Path certPath = Paths.get(usedPath); + + if (!Files.exists(certPath)) { + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "OKE Workload Authentication Details Provider is not available, as " + + "the certificate file does not exist: " + certPath.toAbsolutePath()); + } + return false; + } + if (Files.isRegularFile(certPath)) { + return true; + } + + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "OKE Workload Authentication Details Provider is not available, as " + + "the certificate file is not a regular file: " + certPath.toAbsolutePath()); + } + return false; + } +} diff --git a/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/OkeWorkloadBuilderProvider.java b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/OkeWorkloadBuilderProvider.java new file mode 100644 index 00000000000..7ad8110dc17 --- /dev/null +++ b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/OkeWorkloadBuilderProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 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.integrations.oci.authentication.okeworkload; + +import java.net.URI; +import java.util.function.Supplier; + +import io.helidon.integrations.oci.OciConfig; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider; +import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider.OkeWorkloadIdentityAuthenticationDetailsProviderBuilder; + +/** + * OKE Workload builder provider, uses the + * {@link OkeWorkloadIdentityAuthenticationDetailsProviderBuilder}. + */ +@Service.Provider +class OkeWorkloadBuilderProvider implements Supplier { + private final OciConfig config; + + OkeWorkloadBuilderProvider(OciConfig config) { + this.config = config; + } + + @Override + public OkeWorkloadIdentityAuthenticationDetailsProviderBuilder get() { + var builder = OkeWorkloadIdentityAuthenticationDetailsProvider.builder() + .timeoutForEachRetry((int) config.authenticationTimeout().toMillis()); + + config.imdsBaseUri() + .map(URI::toString) + .ifPresent(builder::metadataBaseUrl); + + return builder; + } + +} diff --git a/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/package-info.java b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/package-info.java new file mode 100644 index 00000000000..e0a456f85eb --- /dev/null +++ b/integrations/oci/authentication/oke-workload/src/main/java/io/helidon/integrations/oci/authentication/okeworkload/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on OKE workload identity. + */ +package io.helidon.integrations.oci.authentication.okeworkload; diff --git a/integrations/oci/authentication/oke-workload/src/main/java/module-info.java b/integrations/oci/authentication/oke-workload/src/main/java/module-info.java new file mode 100644 index 00000000000..3387db8f717 --- /dev/null +++ b/integrations/oci/authentication/oke-workload/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on OKE workload identity. + */ +module io.helidon.integrations.oci.authentication.okeworkload { + requires io.helidon.common; + requires io.helidon.service.registry; + requires io.helidon.integrations.oci; + + requires oci.java.sdk.common; + requires oci.java.sdk.addons.oke.workload.identity; + + exports io.helidon.integrations.oci.authentication.okeworkload; +} diff --git a/integrations/oci/authentication/pom.xml b/integrations/oci/authentication/pom.xml new file mode 100644 index 00000000000..ab0aa0721c6 --- /dev/null +++ b/integrations/oci/authentication/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + + io.helidon.integrations.oci + helidon-integrations-oci-project + 4.1.0-SNAPSHOT + + pom + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-project + Helidon Integrations OCI Authentication Project + + + Additional OCI Authentication providers + + + + instance + resource + oke-workload + + diff --git a/integrations/oci/authentication/resource/pom.xml b/integrations/oci/authentication/resource/pom.xml new file mode 100644 index 00000000000..3a20556adc3 --- /dev/null +++ b/integrations/oci/authentication/resource/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + io.helidon.integrations.oci.authentication + helidon-integrations-oci-authentication-project + 4.1.0-SNAPSHOT + + helidon-integrations-oci-authentication-resource + Helidon Integrations OCI Resource Instance + + + Authentication based on Resource principal + + + + + io.helidon.integrations.oci + helidon-integrations-oci + + + io.helidon.service + helidon-service-registry + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-jdk14 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyResourcePrincipal.java b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/AuthenticationMethodResourcePrincipal.java similarity index 65% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyResourcePrincipal.java rename to integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/AuthenticationMethodResourcePrincipal.java index 2b856fc00a5..cb0da238fcd 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyResourcePrincipal.java +++ b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/AuthenticationMethodResourcePrincipal.java @@ -14,41 +14,41 @@ * limitations under the License. */ -package io.helidon.integrations.oci; +package io.helidon.integrations.oci.authentication.resource; import java.lang.System.Logger.Level; -import java.net.URI; import java.util.Optional; import io.helidon.common.LazyValue; import io.helidon.common.Weight; import io.helidon.common.Weighted; -import io.helidon.integrations.oci.spi.OciAtnStrategy; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; import io.helidon.service.registry.Service; import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; -import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider.ResourcePrincipalAuthenticationDetailsProviderBuilder; /** - * Resource authentication strategy, uses the {@link com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider}. + * Resource principal authentication method, uses the {@link com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider}. */ @Weight(Weighted.DEFAULT_WEIGHT - 30) @Service.Provider -class AtnStrategyResourcePrincipal implements OciAtnStrategy { - static final String RESOURCE_PRINCIPAL_VERSION_ENV_VAR = "OCI_RESOURCE_PRINCIPAL_VERSION"; - static final String STRATEGY = "resource-principal"; +class AuthenticationMethodResourcePrincipal implements OciAuthenticationMethod { + private static final System.Logger LOGGER = System.getLogger(AuthenticationMethodResourcePrincipal.class.getName()); + private static final String RESOURCE_PRINCIPAL_VERSION_ENV_VAR = "OCI_RESOURCE_PRINCIPAL_VERSION"; + private static final String METHOD = "resource-principal"; + - private static final System.Logger LOGGER = System.getLogger(AtnStrategyResourcePrincipal.class.getName()); private final LazyValue> provider; - AtnStrategyResourcePrincipal(OciConfig config) { - provider = createProvider(config); + AuthenticationMethodResourcePrincipal(ResourcePrincipalAuthenticationDetailsProviderBuilder builder) { + provider = createProvider(builder); } @Override - public String strategy() { - return STRATEGY; + public String method() { + return METHOD; } @Override @@ -56,7 +56,9 @@ public Optional provider() { return provider.get(); } - private static LazyValue> createProvider(OciConfig config) { + private static LazyValue> + createProvider(ResourcePrincipalAuthenticationDetailsProviderBuilder builder) { + return LazyValue.create(() -> { // https://github.com/oracle/oci-java-sdk/blob/v2.19.0/bmc-common/src/main/java/com/oracle/bmc/auth/ResourcePrincipalAuthenticationDetailsProvider.java#L246-L251 if (System.getenv(RESOURCE_PRINCIPAL_VERSION_ENV_VAR) == null) { @@ -66,14 +68,6 @@ private static LazyValue> create } return Optional.empty(); } - var builder = ResourcePrincipalAuthenticationDetailsProvider.builder() - .timeoutForEachRetry((int) config.atnTimeout().toMillis()); - - // we expect the full metadata base URI (including http:// and /opc/v2/) - config.imdsBaseUri() - .map(URI::toString) - .ifPresent(builder::metadataBaseUrl); - return Optional.of(builder.build()); }); } diff --git a/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/ResourcePrincipalBuilderProvider.java b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/ResourcePrincipalBuilderProvider.java new file mode 100644 index 00000000000..3967bb8ca99 --- /dev/null +++ b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/ResourcePrincipalBuilderProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 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.integrations.oci.authentication.resource; + +import java.net.URI; +import java.util.function.Supplier; + +import io.helidon.integrations.oci.OciConfig; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider.ResourcePrincipalAuthenticationDetailsProviderBuilder; + +/** + * Instance principal builder provider, uses the + * {@link ResourcePrincipalAuthenticationDetailsProviderBuilder}. + */ +@Service.Provider +class ResourcePrincipalBuilderProvider implements Supplier { + private final OciConfig config; + + ResourcePrincipalBuilderProvider(OciConfig config) { + this.config = config; + } + + @Override + public ResourcePrincipalAuthenticationDetailsProviderBuilder get() { + var builder = ResourcePrincipalAuthenticationDetailsProvider.builder() + .timeoutForEachRetry((int) config.authenticationTimeout().toMillis()); + + config.imdsBaseUri() + .map(URI::toString) + .ifPresent(builder::metadataBaseUrl); + + return builder; + } + +} diff --git a/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/package-info.java b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/package-info.java new file mode 100644 index 00000000000..11d321b819f --- /dev/null +++ b/integrations/oci/authentication/resource/src/main/java/io/helidon/integrations/oci/authentication/resource/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on resource principal. + */ +package io.helidon.integrations.oci.authentication.resource; diff --git a/integrations/oci/authentication/resource/src/main/java/module-info.java b/integrations/oci/authentication/resource/src/main/java/module-info.java new file mode 100644 index 00000000000..bbf63daeafb --- /dev/null +++ b/integrations/oci/authentication/resource/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Support for OCI authentication method based on resource principal. + */ +module io.helidon.integrations.oci.authentication.resource { + requires io.helidon.common; + requires io.helidon.service.registry; + requires io.helidon.integrations.oci; + + requires oci.java.sdk.common; + + exports io.helidon.integrations.oci.authentication.resource; +} diff --git a/integrations/oci/oci/README.md b/integrations/oci/oci/README.md index 21e5204a2ca..588c557605d 100644 --- a/integrations/oci/oci/README.md +++ b/integrations/oci/oci/README.md @@ -6,31 +6,68 @@ The module uses (internally) a service of type `OciConfig`. This instance is use Note that this service can be customized, if a provider with higher weight is created. The default service implementation uses environment variables, system properties, and a configuration file `oci-config.yaml` on file system, or on classpath (Weight: default - 10). -This module provides two services that can be used by other modules. +The configuration options (main): +```yaml +helidon.oci: + authentication-method: "auto" # can select a specific authentication method to use, use auto to choose from available + allowed-authentication-methods: ["config", "config-file"] # limit the list of authentication methods to try with auto + authentication: # specific configuration of authentication methods + config-file: # all details in a config file + profile: "DEFAULT" # optional + path: "/custom/path/.oci/config" # optional + config: # all details here in config + region: "some-region-id" + fingerprint: "0A0B..." + tenant-id: "ocid" + user-id: "ocid" + private-key: + resource-path: "/on/classpath/private-key.pem" + session-token: # same as config + session token and some additional configuration + session-token: "token" + session-lifetime-hours: 8 +``` + +This module provides a few services that can be used by other modules. ## Authentication Details Provider Any service can have a dependency on `com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider`, and it can be looked up using Service Registry lookup methods. -The provider is looked up by using instances of `io.helidon.integrations.oci.spi.OciAtnStrategy`. The first service instance to provide an authentication provider is used, and no other service instance is called. +The provider is looked up by using instances of `io.helidon.integrations.oci.spi.OciAuthenticationMethod`. The first service instance to provide an authentication provider is used, and no other service instance is called. The following out-of-the-box implementations exist: -- Config based provider: uses `OciConfig` to create authentication provider; Weight: default - 10 -- Config file based provider: uses OCI config file (i.e. `~/.oci/config`) to create authentication provider; both file location and profile can be configured through `OciConfig`, Weight: default - 20 -- Resource principal provider: uses resource principal authentication provider; Weight: default - 30 -- Instance principal provider: uses instance principal authentication provider; Weight: default - 40 +| Provider | Weight | Description | +|--------------------|--------|---------------------------------------------------------------| +| Config | 90 | Uses `OciConfig` | +| Session Token | 85 | Uses `OciConfig` or config file, if it contains session token | +| Config File | 80 | Uses `~/.oci/config` file | +| Resource Principal | 70 | Resource principal (such as functions) | +| Instance Principal | 60 | Principal of the compute instance | To create a custom instance of authentication details provider, just create a new service for service registry with default or higher weight that provides an instance of the `AbstractAuthenticationDetailsProvider` (ServiceRegistry requires setup of annotation processors, see this module's pom file). +## Authentication Details Provider Builders + +The following builders can be used as dependencies (and discovered from `ServiceRegistry`). The builders will have as much +information as available filled in: + +- `com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider.SessionTokenAuthenticationDetailsProviderBuilder` +- `com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider.InstancePrincipalsAuthenticationDetailsProviderBuilder` +- `com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider.ResourcePrincipalAuthenticationDetailsProviderBuilder` +- `com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider.OkeWorkloadIdentityAuthenticationDetailsProviderBuilder` + ## Region Any service can have a dependency on `com.oracle.bmc.Region`, and it can be looked up using Service Registry lookup methods. +Important note: authentication details providers MUST NOT have a dependency on `Region`, as it may be provided by authentication +details provider (this would create a cyclic dependency). + Region is discovered by using instances of `io.helidon.integrations.oci.spi.OciRegion`. The first service instance to provide a region is used, and no other service instance is called. diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnDetailsProvider.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpProvider.java similarity index 57% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnDetailsProvider.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpProvider.java index 9a870d6383c..2f850cc62f3 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnDetailsProvider.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpProvider.java @@ -23,25 +23,25 @@ import io.helidon.common.LazyValue; import io.helidon.common.config.ConfigException; -import io.helidon.integrations.oci.spi.OciAtnStrategy; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; import io.helidon.service.registry.Service; import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; @Service.Provider @Service.ExternalContracts(AbstractAuthenticationDetailsProvider.class) -class AtnDetailsProvider implements Supplier> { +class AdpProvider implements Supplier> { private final LazyValue> provider; - AtnDetailsProvider(OciConfig ociConfig, List atnDetailsProviders) { - String chosenStrategy = ociConfig.atnStrategy(); + AdpProvider(OciConfig ociConfig, List atnDetailsProviders) { + String chosenAtnMethod = ociConfig.authenticationMethod(); LazyValue> providerLazyValue = null; - if (OciConfigBlueprint.STRATEGY_AUTO.equals(chosenStrategy)) { + if (OciConfigBlueprint.AUTHENTICATION_METHOD_AUTO.equals(chosenAtnMethod)) { // auto, chose from existing providerLazyValue = LazyValue.create(() -> { - for (OciAtnStrategy atnDetailsProvider : atnDetailsProviders) { + for (OciAuthenticationMethod atnDetailsProvider : atnDetailsProviders) { Optional provider = atnDetailsProvider.provider(); if (provider.isPresent()) { return provider; @@ -52,22 +52,18 @@ class AtnDetailsProvider implements Supplier strategies = new ArrayList<>(); - for (OciAtnStrategy atnDetailsProvider : atnDetailsProviders) { - if (chosenStrategy.equals(atnDetailsProvider.strategy())) { - providerLazyValue = LazyValue.create(() -> Optional.of(atnDetailsProvider.provider() - .orElseThrow(() -> new ConfigException("Strategy \"" - + chosenStrategy - + "\" did not provide an authentication provider, " - + "yet it is requested through configuration.")))); + for (OciAuthenticationMethod atnDetailsProvider : atnDetailsProviders) { + if (chosenAtnMethod.equals(atnDetailsProvider.method())) { + providerLazyValue = LazyValue.create(() -> toProvider(atnDetailsProvider, chosenAtnMethod)); break; } - strategies.add(atnDetailsProvider.strategy()); + strategies.add(atnDetailsProvider.method()); } if (providerLazyValue == null) { - throw new ConfigException("There is a strategy chosen for OCI authentication: \"" + chosenStrategy - + "\", yet there is not provider that can provide that strategy. Supported " - + "strategies: " + strategies); + throw new ConfigException("There is an OCI authentication method chosen: \"" + chosenAtnMethod + + "\", yet there is not provider that can provide this method. Supported " + + "methods: " + strategies); } } @@ -78,4 +74,12 @@ class AtnDetailsProvider implements Supplier get() { return provider.get(); } + + private Optional toProvider(OciAuthenticationMethod atnDetailsProvider, + String chosenMethod) { + return Optional.of(atnDetailsProvider.provider() + .orElseThrow(() -> new ConfigException( + "Authentication method \"" + chosenMethod + "\" did not provide an " + + "authentication provider, yet it is requested through configuration."))); + } } diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpSessionTokenBuilderProvider.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpSessionTokenBuilderProvider.java new file mode 100644 index 00000000000..b20b600864e --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AdpSessionTokenBuilderProvider.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.common.config.ConfigException; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.ConfigFileReader.ConfigFile; +import com.oracle.bmc.Region; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider.SessionTokenAuthenticationDetailsProviderBuilder; + +@Service.Provider +class AdpSessionTokenBuilderProvider implements Supplier { + private static final LazyValue DEFAULT_SCHEDULER = + LazyValue.create(Executors::newSingleThreadScheduledExecutor); + + private final OciConfig config; + private final Supplier> configFileSupplier; + + AdpSessionTokenBuilderProvider(OciConfig config, + Supplier> configFileSupplier) { + + this.config = config; + this.configFileSupplier = configFileSupplier; + } + + @Override + public SessionTokenAuthenticationDetailsProviderBuilder get() { + var builder = SessionTokenAuthenticationDetailsProvider.builder(); + + updateFromConfigFile(builder); + updateFromConfig(builder); + + return builder; + } + + Optional value(ConfigFile file, String key) { + return Optional.ofNullable(file.get(key)); + } + + private void updateFromConfig(SessionTokenAuthenticationDetailsProviderBuilder builder) { + Optional maybeSessionTokenConfig = config.sessionTokenMethodConfig(); + if (maybeSessionTokenConfig.isEmpty()) { + return; + } + SessionTokenMethodConfig sessionTokenConfig = maybeSessionTokenConfig.get(); + + builder.fingerprint(sessionTokenConfig.fingerprint()); + sessionTokenConfig.passphrase() + .map(String::new) + .ifPresent(builder::passPhrase); + sessionTokenConfig.privateKeyPath() + .map(Path::toString) + .ifPresent(builder::privateKeyFilePath); + + builder.region(sessionTokenConfig.region()); + builder.tenantId(sessionTokenConfig.tenantId()); + builder.userId(sessionTokenConfig.userId()); + + builder.timeUnit(TimeUnit.MILLISECONDS); + sessionTokenConfig.initialRefreshDelay() + .map(Duration::toMillis) + .ifPresent(builder::initialRefreshDelay); + + sessionTokenConfig.refreshPeriod() + .map(Duration::toMillis) + .ifPresent(builder::refreshPeriod); + sessionTokenConfig.sessionLifetimeHours() + .ifPresent(builder::sessionLifetimeHours); + + builder.scheduler(sessionTokenConfig.scheduler().orElseGet(DEFAULT_SCHEDULER)); + + /* + Session token + */ + Optional sessionToken = sessionTokenConfig.sessionToken(); + Optional sessionTokenPath = sessionTokenConfig.sessionTokenPath(); + + if (sessionToken.isEmpty() && sessionTokenPath.isEmpty()) { + throw new ConfigException("When configuring session token authentication, either session token or session token " + + "path must be provided"); + } + if (sessionToken.isPresent()) { + builder.sessionToken(sessionToken.get()); + } else { + builder.sessionTokenFilePath(sessionTokenPath.get().toString()); + } + } + + private void updateFromConfigFile(SessionTokenAuthenticationDetailsProviderBuilder builder) { + Optional maybeConfigFile = configFileSupplier.get(); + if (maybeConfigFile.isEmpty()) { + return; + } + ConfigFile configFile = maybeConfigFile.get(); + + value(configFile, "security_token_file") + .ifPresent(builder::sessionTokenFilePath); + value(configFile, "tenancy") + .ifPresent(builder::tenantId); + value(configFile, "key_file") + .ifPresent(builder::privateKeyFilePath); + value(configFile, "fingerprint") + .ifPresent(builder::fingerprint); + value(configFile, "pass_phrase") + .ifPresent(builder::passPhrase); + value(configFile, "user") + .ifPresent(builder::userId); + + Region region = ConfigFileAuthenticationDetailsProvider.getRegionFromConfigFile(configFile); + if (region != null) { + builder.region(region); + } + } +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfigFile.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfigFile.java deleted file mode 100644 index ae39f4e9f34..00000000000 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfigFile.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2024 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.integrations.oci; - -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.util.Optional; - -import io.helidon.common.LazyValue; -import io.helidon.common.Weight; -import io.helidon.common.Weighted; -import io.helidon.integrations.oci.spi.OciAtnStrategy; -import io.helidon.service.registry.Service; - -import com.oracle.bmc.ConfigFileReader; -import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; -import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; - -/** - * Config file based authentication strategy, uses the {@link com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider}. - */ -@Weight(Weighted.DEFAULT_WEIGHT - 20) -@Service.Provider -class AtnStrategyConfigFile implements OciAtnStrategy { - static final String DEFAULT_PROFILE_NAME = "DEFAULT"; - static final String STRATEGY = "config-file"; - - private static final System.Logger LOGGER = System.getLogger(AtnStrategyConfigFile.class.getName()); - - private final LazyValue> provider; - - AtnStrategyConfigFile(OciConfig config) { - provider = createProvider(config); - } - - @Override - public String strategy() { - return STRATEGY; - } - - @Override - public Optional provider() { - return provider.get(); - } - - private static LazyValue> createProvider(OciConfig config) { - return LazyValue.create(() -> { - // there are two options to override - the path to config file, and the profile - var strategyConfig = config.configFileStrategyConfig(); - String profile = strategyConfig.map(ConfigFileStrategyConfigBlueprint::profile) - .orElse(DEFAULT_PROFILE_NAME); - String configFilePath = strategyConfig.flatMap(ConfigFileStrategyConfigBlueprint::path) - .orElse(null); - - try { - ConfigFileReader.ConfigFile configFile; - if (configFilePath == null) { - configFile = ConfigFileReader.parseDefault(profile); - } else { - configFile = ConfigFileReader.parse(configFilePath, profile); - } - return Optional.of(new ConfigFileAuthenticationDetailsProvider(configFile)); - } catch (IOException e) { - if (LOGGER.isLoggable(Level.TRACE)) { - LOGGER.log(Level.TRACE, "Cannot parse config file. Location: " + configFilePath + ", profile: " + profile, e); - } - return Optional.empty(); - } - }); - } -} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyInstancePrincipal.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyInstancePrincipal.java deleted file mode 100644 index 5a9730f108b..00000000000 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyInstancePrincipal.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2024 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.integrations.oci; - -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.net.InetAddress; -import java.net.URI; -import java.time.Duration; -import java.util.Optional; - -import io.helidon.common.LazyValue; -import io.helidon.common.Weight; -import io.helidon.common.Weighted; -import io.helidon.integrations.oci.spi.OciAtnStrategy; -import io.helidon.service.registry.Service; - -import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; -import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; - -import static io.helidon.integrations.oci.OciConfigSupport.IMDS_HOSTNAME; - -/** - * Instance principal authentication strategy, uses the - * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider}. - */ -@Weight(Weighted.DEFAULT_WEIGHT - 40) -@Service.Provider -class AtnStrategyInstancePrincipal implements OciAtnStrategy { - static final String STRATEGY = "instance-principal"; - - private static final System.Logger LOGGER = System.getLogger(AtnStrategyInstancePrincipal.class.getName()); - - private final LazyValue> provider; - - AtnStrategyInstancePrincipal(OciConfig config) { - provider = createProvider(config); - } - - @Override - public String strategy() { - return STRATEGY; - } - - @Override - public Optional provider() { - return provider.get(); - } - - private static LazyValue> createProvider(OciConfig config) { - return LazyValue.create(() -> { - if (imdsAvailable(config)) { - var builder = InstancePrincipalsAuthenticationDetailsProvider.builder() - .timeoutForEachRetry((int) config.atnTimeout().toMillis()); - - config.imdsBaseUri() - .map(URI::toString) - .ifPresent(builder::metadataBaseUrl); - - return Optional.of(builder.build()); - } - return Optional.empty(); - }); - } - - static boolean imdsAvailable(OciConfig config) { - Duration timeout = config.imdsTimeout(); - - try { - if (InetAddress.getByName(config.imdsBaseUri().map(URI::getHost).orElse(IMDS_HOSTNAME)) - .isReachable((int) timeout.toMillis())) { - return RegionProviderSdk.regionFromImds(config) != null; - } - return false; - } catch (IOException e) { - LOGGER.log(Level.TRACE, - "imds service is not reachable, or timed out for address: " + IMDS_HOSTNAME + ", instance principal " - + "strategy is not available.", - e); - return false; - } - } -} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfig.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfig.java similarity index 68% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfig.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfig.java index 8c33237de6f..2be49bd2136 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AtnStrategyConfig.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfig.java @@ -16,13 +16,14 @@ package io.helidon.integrations.oci; +import java.lang.System.Logger.Level; import java.util.Optional; import io.helidon.common.LazyValue; import io.helidon.common.Weight; import io.helidon.common.Weighted; import io.helidon.common.configurable.Resource; -import io.helidon.integrations.oci.spi.OciAtnStrategy; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; import io.helidon.service.registry.Service; import com.oracle.bmc.Region; @@ -31,26 +32,34 @@ import com.oracle.bmc.auth.SimplePrivateKeySupplier; /** - * Config based authentication strategy, uses the {@link com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider}. + * Config based authentication method, uses the {@link com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider}. */ @Weight(Weighted.DEFAULT_WEIGHT - 10) @Service.Provider -class AtnStrategyConfig implements OciAtnStrategy { - static final String STRATEGY = "config"; +class AuthenticationMethodConfig implements OciAuthenticationMethod { + static final String METHOD = "config"; + + private static final System.Logger LOGGER = System.getLogger(AuthenticationMethodConfig.class.getName()); private final LazyValue> provider; - AtnStrategyConfig(OciConfig config) { - provider = config.configStrategyConfig() - .map(configStrategyConfigBlueprint -> LazyValue.create(() -> { - return Optional.of(createProvider(configStrategyConfigBlueprint)); + AuthenticationMethodConfig(OciConfig config) { + provider = config.configMethodConfig() + .map(configMethodConfigBlueprint -> LazyValue.create(() -> { + return Optional.of(createProvider(configMethodConfigBlueprint)); })) - .orElseGet(() -> LazyValue.create(Optional.empty())); + .orElseGet(() -> { + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "Configuration for Config based authentication details provider is" + + " not available."); + } + return LazyValue.create(Optional.empty()); + }); } @Override - public String strategy() { - return STRATEGY; + public String method() { + return METHOD; } @Override @@ -58,7 +67,7 @@ public Optional provider() { return provider.get(); } - private static AbstractAuthenticationDetailsProvider createProvider(ConfigStrategyConfigBlueprint config) { + private static AbstractAuthenticationDetailsProvider createProvider(ConfigMethodConfigBlueprint config) { Region region = Region.fromRegionCodeOrId(config.region()); var builder = SimpleAuthenticationDetailsProvider.builder(); @@ -83,11 +92,12 @@ private static AbstractAuthenticationDetailsProvider createProvider(ConfigStrate builder.privateKeySupplier(new SimplePrivateKeySupplier(keyFile)); } + config.passphrase().ifPresent(builder::passphraseCharacters); + return builder.region(region) .tenantId(config.tenantId()) .userId(config.userId()) .fingerprint(config.fingerprint()) - .passphraseCharacters(config.passphrase()) .build(); } diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfigFile.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfigFile.java new file mode 100644 index 00000000000..4635a8f9fd8 --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodConfigFile.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; + +/** + * Config file based authentication method, uses the {@link com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 20) +@Service.Provider +class AuthenticationMethodConfigFile implements OciAuthenticationMethod { + static final String METHOD = "config-file"; + + private final LazyValue> provider; + + AuthenticationMethodConfigFile(Supplier> configFile) { + provider = createProvider(configFile); + } + + @Override + public String method() { + return METHOD; + } + + @Override + public Optional provider() { + return provider.get(); + } + + private static LazyValue> + createProvider(Supplier> configFileSupplier) { + + return LazyValue.create(() -> configFileSupplier.get() + .map(ConfigFileAuthenticationDetailsProvider::new)); + } +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodSessionToken.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodSessionToken.java new file mode 100644 index 00000000000..b4854adf880 --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/AuthenticationMethodSessionToken.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.integrations.oci.spi.OciAuthenticationMethod; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider.SessionTokenAuthenticationDetailsProviderBuilder; + +/** + * Session token authentication method, uses the {@link com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT - 15) +@Service.Provider +class AuthenticationMethodSessionToken implements OciAuthenticationMethod { + static final String DEFAULT_PROFILE_NAME = "DEFAULT"; + static final String METHOD = "session-token"; + + private static final System.Logger LOGGER = System.getLogger(AuthenticationMethodSessionToken.class.getName()); + + private final LazyValue> provider; + + AuthenticationMethodSessionToken(OciConfig config, + Supplier> configFileSupplier, + Supplier builder) { + provider = LazyValue.create(() -> createProvider(config, configFileSupplier, builder)); + } + + @Override + public String method() { + return METHOD; + } + + @Override + public Optional provider() { + return provider.get(); + } + + private static Optional + createProvider(OciConfig config, + Supplier> configFileSupplier, + Supplier builder) { + + /* + Session tokens provide is available if either of the following is true: + - there is authentication.session-token configuration + - there is an OCI config file, and it contains security_token_file + */ + + Optional maybeConfigFile = configFileSupplier.get(); + Optional maybeSessionTokenConfig = config.sessionTokenMethodConfig(); + + if (hasSecurityToken(maybeConfigFile) || maybeSessionTokenConfig.isPresent()) { + try { + return Optional.of(builder.get().build()); + } catch (IOException e) { + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "Cannot create session token authentication provider", e); + } + return Optional.empty(); + } + } + + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "Session token authentication provider is not configured"); + } + return Optional.empty(); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static boolean hasSecurityToken(Optional maybeConfigFile) { + if (maybeConfigFile.isPresent()) { + ConfigFileReader.ConfigFile configFile = maybeConfigFile.get(); + return configFile.get("security_token_file") != null; + } + return false; + } +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileStrategyConfigBlueprint.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileMethodConfigBlueprint.java similarity index 91% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileStrategyConfigBlueprint.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileMethodConfigBlueprint.java index 7ee84c7fb75..5a1a6b3f2da 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileStrategyConfigBlueprint.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileMethodConfigBlueprint.java @@ -23,7 +23,7 @@ @Prototype.Blueprint @Prototype.Configured -interface ConfigFileStrategyConfigBlueprint { +interface ConfigFileMethodConfigBlueprint { /** * The OCI configuration profile path. * @@ -38,6 +38,6 @@ interface ConfigFileStrategyConfigBlueprint { * @return the optional OCI configuration/auth profile name */ @Option.Configured - @Option.Default(AtnStrategyConfigFile.DEFAULT_PROFILE_NAME) + @Option.Default(ConfigFileProvider.DEFAULT_PROFILE_NAME) String profile(); } diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileProvider.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileProvider.java new file mode 100644 index 00000000000..f2ff288f4d7 --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigFileProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.io.IOException; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.service.registry.Service; + +import com.oracle.bmc.ConfigFileReader; + +@Service.Provider +@Service.ExternalContracts(ConfigFileReader.ConfigFile.class) +class ConfigFileProvider implements Supplier> { + static final String DEFAULT_PROFILE_NAME = "DEFAULT"; + + private static final System.Logger LOGGER = System.getLogger(ConfigFileProvider.class.getName()); + + private final LazyValue> value; + + ConfigFileProvider(OciConfig config) { + value = LazyValue.create(() -> findConfigFile(config)); + } + + @Override + public Optional get() { + return value.get(); + } + + private Optional findConfigFile(OciConfig config) { + var atnMethodConfig = config.configFileMethodConfig(); + // there are two options to override - the path to config file, and the profile + String profile = atnMethodConfig.map(ConfigFileMethodConfigBlueprint::profile) + .orElse(DEFAULT_PROFILE_NAME); + String configFilePath = atnMethodConfig.flatMap(ConfigFileMethodConfigBlueprint::path) + .orElse(null); + + try { + ConfigFileReader.ConfigFile configFile; + if (configFilePath == null) { + configFile = ConfigFileReader.parseDefault(profile); + } else { + configFile = ConfigFileReader.parse(configFilePath, profile); + } + return Optional.of(configFile); + } catch (IOException e) { + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, + "Cannot parse config file. Location: " + configFilePath + ", profile: " + profile, + e); + } + return Optional.empty(); + } + } +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigStrategyConfigBlueprint.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigMethodConfigBlueprint.java similarity index 95% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigStrategyConfigBlueprint.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigMethodConfigBlueprint.java index 968688f1587..a8561c062dd 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigStrategyConfigBlueprint.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/ConfigMethodConfigBlueprint.java @@ -23,11 +23,11 @@ import io.helidon.common.configurable.Resource; /** - * Configuration of the {@code config} authentication strategy. + * Configuration of the {@code config} authentication method. */ @Prototype.Blueprint @Prototype.Configured -interface ConfigStrategyConfigBlueprint { +interface ConfigMethodConfigBlueprint { /** * The OCI region. * @@ -70,7 +70,7 @@ interface ConfigStrategyConfigBlueprint { */ @Option.Configured @Option.Confidential - char[] passphrase(); + Optional passphrase(); /** * The OCI tenant id. diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/HelidonOci.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/HelidonOci.java new file mode 100644 index 00000000000..b843797618f --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/HelidonOci.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.time.Duration; + +import static io.helidon.integrations.oci.OciConfigSupport.IMDS_HOSTNAME; + +/** + * Helper methods for OCI integration. + */ +public final class HelidonOci { + private static final System.Logger LOGGER = System.getLogger(HelidonOci.class.getName()); + + private HelidonOci() { + } + + /** + * Check whether IMDS (metadata service endpoint) is available or not. + * + * @param config OCI meta configuration, allowing a customized IMDS endpoint + * @return whether the metadata service is available + */ + public static boolean imdsAvailable(OciConfig config) { + Duration timeout = config.imdsTimeout(); + + try { + if (InetAddress.getByName(config.imdsBaseUri().map(URI::getHost).orElse(IMDS_HOSTNAME)) + .isReachable((int) timeout.toMillis())) { + return RegionProviderSdk.regionFromImds(config) != null; + } + return false; + } catch (IOException e) { + LOGGER.log(System.Logger.Level.TRACE, + "IMDS service is not reachable, or timed out for address: " + + IMDS_HOSTNAME + ".", + e); + return false; + } + } +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/OciConfigBlueprint.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/OciConfigBlueprint.java index cdc554afbeb..1dad85f8fa3 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/OciConfigBlueprint.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/OciConfigBlueprint.java @@ -23,6 +23,7 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; +import io.helidon.common.config.Config; import com.oracle.bmc.Region; @@ -36,10 +37,10 @@ @Prototype.CustomMethods(OciConfigSupport.class) interface OciConfigBlueprint { /** - * Default authentication strategy. The default is to use automatic discovery - i.e. cycle through possible + * Default authentication method. The default is to use automatic discovery - i.e. cycle through possible * providers until one yields an authentication details provider instance. */ - String STRATEGY_AUTO = "auto"; + String AUTHENTICATION_METHOD_AUTO = "auto"; /** * Explicit region. The configured region will be used by region provider. @@ -51,58 +52,70 @@ interface OciConfigBlueprint { Optional region(); /** - * Authentication strategy to use. If the configured strategy is not available, an exception + * Authentication method to use. If the configured method is not available, an exception * would be thrown for OCI related services. *

* Known and supported authentication strategies for public OCI: *

    - *
  • {@value #STRATEGY_AUTO} - use the list of {@link #allowedAtnStrategies()} (in the provided order), and choose - * the first one - * capable of providing data
  • - *
  • {@value AtnStrategyConfig#STRATEGY} - + *
  • {@value #AUTHENTICATION_METHOD_AUTO} - use the list of {@link io.helidon.integrations.oci.OciConfig#allowedAuthenticationMethods()} + * (in the provided order), and choose the first one capable of providing data
  • + *
  • {@value AuthenticationMethodConfig#METHOD} - * use configuration of the application to obtain values needed to set up connectivity, uses * {@link com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider}
  • - *
  • {@value AtnStrategyConfigFile#STRATEGY} - use configuration file of OCI ({@code home/.oci/config}), uses + *
  • {@value AuthenticationMethodConfigFile#METHOD} - use configuration file of OCI ({@code home/.oci/config}), uses * {@link com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider}
  • - *
  • {@value AtnStrategyResourcePrincipal#STRATEGY} - use identity of the OCI resource the service is executed on + *
  • {@code resource-principal} - use identity of the OCI resource the service is executed on * (fn), uses - * {@link com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider}
  • - *
  • {@value AtnStrategyInstancePrincipal#STRATEGY} - use identity of the OCI instance the service is running on, uses - * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider}
  • + * {@link com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider}, and is available in a + * separate module {@code helidon-integrations-oci-authentication-resource} + *
  • {@code instance-principal} - use identity of the OCI instance the service is running on, uses + * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider}, and is available in a + * separate module {@code helidon-integrations-oci-authentication-resource}
  • + *
  • {@code workload} - use workload identity of the OCI Kubernetes workload, available in a + * separate module {@code helidon-integrations-oci-authentication-workload}
  • *
* - * @return the authentication strategy to apply + * @return the authentication method to apply */ @Option.Configured - @Option.Default(STRATEGY_AUTO) - String atnStrategy(); + @Option.Default(AUTHENTICATION_METHOD_AUTO) + String authenticationMethod(); /** - * List of attempted authentication strategies in case {@link #atnStrategy()} is set to {@value #STRATEGY_AUTO}. + * List of attempted authentication strategies in case {@link io.helidon.integrations.oci.OciConfig#authenticationMethod()} is set + * to {@value #AUTHENTICATION_METHOD_AUTO}. *

* In case the list is empty, all available strategies will be tried, ordered by their {@link io.helidon.common.Weight} * * @return list of authentication strategies to be tried - * @see #atnStrategy() + * @see io.helidon.integrations.oci.OciConfig#authenticationMethod() */ @Option.Configured - List allowedAtnStrategies(); + List allowedAuthenticationMethods(); /** - * Config strategy configuration (if provided and used). + * Config method configuration (if provided and used). * - * @return information needed for config {@link #atnStrategy()} + * @return information needed for config {@link io.helidon.integrations.oci.OciConfig#authenticationMethod()} */ - @Option.Configured("config-strategy") - Optional configStrategyConfig(); + @Option.Configured("authentication.config") + Optional configMethodConfig(); /** - * Config file strategy configuration (if provided and used). + * Config file method configuration (if provided and used). * - * @return information to customize config for {@link #atnStrategy()} + * @return information to customize config for {@link io.helidon.integrations.oci.OciConfig#authenticationMethod()} */ - @Option.Configured("config-file-strategy") - Optional configFileStrategyConfig(); + @Option.Configured("authentication.config-file") + Optional configFileMethodConfig(); + + /** + * Session token method configuration (if provided and used). + * + * @return information to customize config for {@link io.helidon.integrations.oci.OciConfig#authenticationMethod()} + */ + @Option.Configured("authentication.session-token") + Optional sessionTokenMethodConfig(); /** * The OCI IMDS connection timeout. This is used to auto-detect availability. @@ -116,7 +129,7 @@ interface OciConfigBlueprint { Duration imdsTimeout(); /** - * The OCI IMDS URI (http URL pointing to the metadata service, if customization needed. + * The OCI IMDS URI (http URL pointing to the metadata service, if customization needed). * * @return the OCI IMDS URI */ @@ -132,5 +145,12 @@ interface OciConfigBlueprint { */ @Option.Configured @Option.Default("PT10S") - Duration atnTimeout(); + Duration authenticationTimeout(); + + /** + * Get the config used to update the builder. + * + * @return configuration + */ + Optional config(); } diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAtnStrategy.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAuthenticationMethod.java similarity index 83% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAtnStrategy.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAuthenticationMethod.java index f8707b31726..db5dbb4142e 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAtnStrategy.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderAuthenticationMethod.java @@ -29,12 +29,16 @@ import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; import com.oracle.bmc.auth.RegionProvider; +/** + * Region provider that uses an available OCI authentication method, if it yields an + * authentication details provider that implements a region provider. + */ @Service.Provider @Weight(Weighted.DEFAULT_WEIGHT - 20) -class RegionProviderAtnStrategy implements OciRegion { +class RegionProviderAuthenticationMethod implements OciRegion { private final LazyValue> region; - RegionProviderAtnStrategy(Supplier> atnProvider) { + RegionProviderAuthenticationMethod(Supplier> atnProvider) { this.region = LazyValue.create(() -> { var provider = atnProvider.get(); diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderSdk.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderSdk.java index 9d8034a8abe..123c06881fe 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderSdk.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/RegionProviderSdk.java @@ -42,7 +42,7 @@ class RegionProviderSdk implements OciRegion { * We want a different way to get the region if available. */ static Region regionFromImds(OciConfig ociConfig) { - if (AtnStrategyInstancePrincipal.imdsAvailable(ociConfig)) { + if (HelidonOci.imdsAvailable(ociConfig)) { Optional uri = ociConfig.imdsBaseUri(); return uri.map(URI::toString) .map(Region::getRegionFromImds) diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/SessionTokenMethodConfigBlueprint.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/SessionTokenMethodConfigBlueprint.java new file mode 100644 index 00000000000..70d8350833a --- /dev/null +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/SessionTokenMethodConfigBlueprint.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024 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.integrations.oci; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * Configuration of the {@code config} authentication method. + */ +@Prototype.Blueprint +@Prototype.Configured +interface SessionTokenMethodConfigBlueprint { + /** + * The OCI region. + * + * @return the OCI region + */ + @Option.Configured + String region(); + + /** + * The OCI authentication fingerprint. + *

+ * This configuration property must be provided in order to set the API signing key's fingerprint. + * See {@link com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getFingerprint()} for more details. + * + * @return the OCI authentication fingerprint + */ + @Option.Configured + String fingerprint(); + + /** + * The OCI authentication passphrase. + *

+ * This property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPassphraseCharacters()}. + * + * @return the OCI authentication passphrase + */ + @Option.Configured + @Option.Confidential + Optional passphrase(); + + /** + * The OCI tenant id. + *

+ * This property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getTenantId()}. + * + * @return the OCI tenant id + */ + @Option.Configured + String tenantId(); + + /** + * The OCI user id. + *

+ * This property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getUserId()}. + * + * @return the OCI user id + */ + @Option.Configured + String userId(); + + /** + * The OCI authentication private key resource. + * A resource can be defined as a resource on classpath, file on the file system, + * base64 encoded text value in config, or plain-text value in config. + *

+ * If not defined, we will use {@code ".oci/sessions/DEFAULT/oci_api_key.pem} file in user home directory. + * + * @return the OCI authentication key file + */ + @Option.Configured + Optional privateKeyPath(); + + /** + * Session token path. + * If both this value, and {@link #sessionToken()} is defined, the value of {@link #sessionToken()} is used. + * + * @return session token path + */ + @Option.Configured + Optional sessionTokenPath(); + + /** + * Session token value. + * If both this value, and {@link #sessionTokenPath()} is defined, this value is used. + * + * @return session token + */ + @Option.Configured + Optional sessionToken(); + + /** + * Delay of the first refresh. + * Defaults to 0, to refresh immediately (implemented in the authentication details provider). + * + * @return initial refresh delay + * @see com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider.SessionTokenAuthenticationDetailsProviderBuilder#initialRefreshDelay(long) + */ + @Option.Configured + Optional initialRefreshDelay(); + + /** + * Refresh period, i.e. how often refresh occurs. + * Defaults to 55 minutes (implemented in the authentication details provider). + * + * @return refresh period + * @see com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider.SessionTokenAuthenticationDetailsProviderBuilder#refreshPeriod(long) + */ + @Option.Configured + Optional refreshPeriod(); + + /** + * Maximal lifetime of a session. + * Defaults to (and maximum is) 24 hours. + * Can only be set to a lower value. + * + * @return lifetime of a session in hours + */ + @Option.Configured + Optional sessionLifetimeHours(); + + /** + * Customize the scheduled executor service to use for scheduling. + * Defaults to a single thread executor service. + * + * @return scheduled executor service + */ + Optional scheduler(); +} diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAtnStrategy.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAuthenticationMethod.java similarity index 79% rename from integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAtnStrategy.java rename to integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAuthenticationMethod.java index 44f324dbfab..0ca6d2600cd 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAtnStrategy.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/OciAuthenticationMethod.java @@ -27,22 +27,22 @@ *

* This service is implemented by: *

    - *
  • Config based strategy
  • - *
  • Config file based strategy
  • - *
  • Resource principal strategy
  • - *
  • Instance principal strategy
  • + *
  • Config based method
  • + *
  • Config file based method
  • + *
  • Resource principal method
  • + *
  • Instance principal method
  • *
* The first one that provides an instance will be used as the value. * To customize, create your own service with a default or higher weight. */ @Service.Contract -public interface OciAtnStrategy { +public interface OciAuthenticationMethod { /** - * The strategy name, can be used to explicitly select a strategy using configuration. + * The OCI authentication method name, can be used to explicitly select a method using configuration. * - * @return strategy name + * @return OCI authentication method name */ - String strategy(); + String method(); /** * Provide an instance of the {@link com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider} to be used diff --git a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/package-info.java b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/package-info.java index 858027753d0..e57786e087c 100644 --- a/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/package-info.java +++ b/integrations/oci/oci/src/main/java/io/helidon/integrations/oci/spi/package-info.java @@ -17,7 +17,7 @@ /** * Extension points for OCI integration. * - * @see io.helidon.integrations.oci.spi.OciAtnStrategy + * @see io.helidon.integrations.oci.spi.OciAuthenticationMethod * @see io.helidon.integrations.oci.spi.OciRegion */ package io.helidon.integrations.oci.spi; diff --git a/integrations/oci/oci/src/main/java/module-info.java b/integrations/oci/oci/src/main/java/module-info.java index 0175cccaa98..91b06eecdd3 100644 --- a/integrations/oci/oci/src/main/java/module-info.java +++ b/integrations/oci/oci/src/main/java/module-info.java @@ -29,14 +29,14 @@ *
  • Create a file {@code oci-config.yaml} either on classpath, or in the current directory with configuration * required by {@link io.helidon.integrations.oci.OciConfig}, this also requires YAML config parser on classpath.
  • *
  • Add environment variables to override configuration options of {@link io.helidon.integrations.oci.OciConfig}, - * such as {@code OCI_ATNSTRATEGY=config_file}
  • + * such as {@code OCI_AUTHENTICATION_METHOD=config_file} *
  • Add system properties to override configuration options of {@link io.helidon.integrations.oci.OciConfig}, - * such as {@code oci.atnStrategy=config_file}
  • + * such as {@code oci.authenticationMethod=config_file} * * - * To customize authentication details provider, you can implement {@link io.helidon.integrations.oci.spi.OciAtnStrategy} + * To customize authentication details provider, you can implement {@link io.helidon.integrations.oci.spi.OciAuthenticationMethod} * service. The out-of-the-box providers have all less than default weight, and are in the - * following order (strategy: description (weight)): + * following order (authentication method: description (weight)): *