From 7fe76fb3177394d7a37144d32cfae15828ad7eeb Mon Sep 17 00:00:00 2001 From: sourabh_sourabh Date: Wed, 21 May 2025 12:54:15 +0100 Subject: [PATCH] Added JettyConfig with support for ambiguous path separators - Added JettyConfig to customize Jetty's HttpConfiguration - For ambiguous path separators via UriCompliance settings. Issue-ID: CPS-2819 Change-Id: I30cbb14cf31ac808a9a9761cf8dbf9ba0df870ae Signed-off-by: sourabh_sourabh --- .../main/java/org/onap/cps/config/JettyConfig.java | 68 ++++++++++++++++++++++ .../org/onap/cps/config/JettyConfigSpec.groovy | 64 ++++++++++++++++++++ .../controller/NetworkCmProxyControllerSpec.groovy | 11 ++++ 3 files changed, 143 insertions(+) create mode 100644 cps-application/src/main/java/org/onap/cps/config/JettyConfig.java create mode 100644 cps-application/src/test/groovy/org/onap/cps/config/JettyConfigSpec.groovy diff --git a/cps-application/src/main/java/org/onap/cps/config/JettyConfig.java b/cps-application/src/main/java/org/onap/cps/config/JettyConfig.java new file mode 100644 index 0000000000..a080ce60b1 --- /dev/null +++ b/cps-application/src/main/java/org/onap/cps/config/JettyConfig.java @@ -0,0 +1,68 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.config; + +import java.util.EnumSet; +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Configuration; + +/** + * Configures the Jetty server to allow encoded slashes (%2F) within URI path segments. + + * This customization is essential when path parameters may include encoded slashes, + * such as hierarchical identifiers (e.g., {@code SubNetwork=Europe/SubNetwork=Ireland}). + * By permitting the {@code AMBIGUOUS_PATH_SEPARATOR} violation, Jetty accepts these + * encoded slashes without rejecting the request. + * + * @see Jetty Server Compliance Modes + * @see UriCompliance.Violation + */ +@Configuration +public class JettyConfig implements WebServerFactoryCustomizer { + + /** + * Customizes the Jetty server factory to allow encoded slashes in URI paths. + * + * @param jettyServletWebServerFactory the Jetty servlet web server factory to customize + */ + @Override + public void customize(final JettyServletWebServerFactory jettyServletWebServerFactory) { + jettyServletWebServerFactory.addServerCustomizers(server -> { + for (final Connector connector : server.getConnectors()) { + for (final ConnectionFactory connectionFactory : connector.getConnectionFactories()) { + if (connectionFactory instanceof HttpConnectionFactory) { + final HttpConfiguration httpConfiguration + = ((HttpConnectionFactory) connectionFactory).getHttpConfiguration(); + httpConfiguration.setUriCompliance(UriCompliance.from(EnumSet.of( + UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR))); + } + } + } + }); + } +} + diff --git a/cps-application/src/test/groovy/org/onap/cps/config/JettyConfigSpec.groovy b/cps-application/src/test/groovy/org/onap/cps/config/JettyConfigSpec.groovy new file mode 100644 index 0000000000..f85a879a31 --- /dev/null +++ b/cps-application/src/test/groovy/org/onap/cps/config/JettyConfigSpec.groovy @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.config + +import org.eclipse.jetty.server.ConnectionFactory +import org.eclipse.jetty.server.Connector +import org.eclipse.jetty.server.HttpConfiguration +import org.eclipse.jetty.server.HttpConnectionFactory +import org.eclipse.jetty.server.Server +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory +import spock.lang.Specification + +class JettyConfigSpec extends Specification { + + def objectUnderTest = new JettyConfig() + def server = Mock(Server) + def connector = Mock(Connector) + + def 'Enable support for ambiguous path separators in Jetty Http configuration'() { + given: 'a Jetty server factory' + def jettyServletWebServerFactory = new JettyServletWebServerFactory() + and: 'a mocked connection factory (Http or Non-Http)' + def connectionFactory = connectionFactoryType == 'http' ? Mock(HttpConnectionFactory) : Mock(ConnectionFactory) + and: 'optional mock for HttpConfiguration if applicable' + def httpConfig = null + if (connectionFactory instanceof HttpConnectionFactory) { + httpConfig = Mock(HttpConfiguration) + connectionFactory.getHttpConfiguration() >> httpConfig + } + and: 'mocked components return expected values' + connector.getConnectionFactories() >> [connectionFactory] + server.getConnectors() >> [connector] + when: 'JettyConfig customization is triggered on the server factory' + objectUnderTest.customize(jettyServletWebServerFactory) + and: 'a server customizer is extracted from the configured factory' + def serverCustomizer = jettyServletWebServerFactory.serverCustomizers.first() + and: 'the customizer is applied to the mocked Jetty server' + serverCustomizer.customize(server) + then: 'only the HTTP configuration is updated to allow ambiguous path separators' + expectedCalls * httpConfig.setUriCompliance({ it.toString().contains('AMBIGUOUS_PATH_SEPARATOR') }) + where: 'type of connection factories' + scenario | connectionFactoryType || expectedCalls + 'HttpConnectionFactory - should configure UriCompliance' | 'http' || 1 + 'Non-HttpConnectionFactory - should do nothing' | 'non-http' || 0 + } +} diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index e934530d6f..8c7f5fbdf6 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -477,6 +477,17 @@ class NetworkCmProxyControllerSpec extends Specification { 'invalid datastore' | 'DELETE' | 'invalid' } + def 'Ensure URI-encoded alternateId with slashes is accepted for #operation - #scenario'() { + given: 'A URI-encoded alternateId that includes slashes' + def alternateIdWithSlashes = '/some/cps/path' + def encodedAlternateId = URLEncoder.encode(alternateIdWithSlashes, 'UTF-8') + def url = "$ncmpBasePathV1/ch/${encodedAlternateId}/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=some-value" + when: 'A passthrough operation is executed on the URL containing the encoded alternateId' + def response = mvc.perform(executeRestOperation('POST', url)).andReturn().response + then: 'The API successfully processes the request and returns the expected HTTP status' + assert response.status == HttpStatus.CREATED.value() + } + def executeRestOperation(operation, url) { if (operation == 'POST') { return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody) -- 2.16.6