Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

The Platform HTTP starter provides Spring Boot auto-configuration for the Camel Platform HTTP component.

== Path variables

A consumer path may declare placeholders, such as `platform-http:/greeting/{name}`, and the matched value is
set as a message header named after the placeholder.

The values are taken from the path Spring Boot matched the request against, so they are percent-decoded and
carry no matrix parameters. A request to `/greeting/John%20Doe;v=1` sets the `name` header to `John Doe`.

The `CamelHttpPath` header is not affected: it reports the raw request path, with the servlet context-path
removed.

== Undertow Access Log

You can enable Undertow access log to be managed by whatever logging library you have in your camel application, you have to set the following parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.RequestPath;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
Expand Down Expand Up @@ -73,27 +76,67 @@ public class SpringBootPlatformHttpBinding extends DefaultHttpBinding {

protected void populateRequestParameters(HttpServletRequest request, Message message) {
super.populateRequestParameters(request, message);
String path = getRawPath(request);
PlatformHttpEndpoint endpoint = (PlatformHttpEndpoint) message.getExchange().getFromEndpoint();
String consumerPath = endpoint.getPath();
if (consumerPath != null && consumerPath.startsWith("/")) {
consumerPath = consumerPath.substring(1);
}
if (consumerPath == null || !useRestMatching(consumerPath)) {
return;
}
String path = getMatchedPath(request);
// skip leading slash
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
if (path != null) {
PlatformHttpEndpoint endpoint = (PlatformHttpEndpoint) message.getExchange().getFromEndpoint();
String consumerPath = endpoint.getPath();
if (consumerPath != null && consumerPath.startsWith("/")) {
consumerPath = consumerPath.substring(1);
}
if (useRestMatching(consumerPath)) {
HttpHelper.evalPlaceholders(message.getHeaders(), path, consumerPath);
}
HttpHelper.evalPlaceholders(message.getHeaders(), path, consumerPath);
}
}

private boolean useRestMatching(String path) {
return path.indexOf('{') > -1;
}

/**
* The request path to evaluate the rest placeholders of the consumer path against.
* <p/>
* Spring matches the request against the {@link RequestPath} it parses from the request, whose segments are
* percent-decoded and stripped of matrix parameters. Evaluating the placeholders against that same path makes the
* headers agree with the path the request was actually matched on, and matches the decoded values the vertx engine
* provides.
* <p/>
* The path is parsed again instead of reading the one Spring cached in the request, because the request is serviced
* on another thread and the dispatch that cached it may already have removed it by then.
*
* @param request the current request
* @return the path the placeholders are evaluated against
*/
private String getMatchedPath(HttpServletRequest request) {
try {
// pathWithinApplication has the context-path (and any servlet path prefix) removed, which is the
// same part getRawPath skips
return toMatchedValue(ServletRequestPathUtils.parse(request).pathWithinApplication());
} catch (Exception e) {
LOG.debug("Cannot parse request path of {}, using the raw path instead", request.getRequestURI(), e);
return getRawPath(request);
}
}

private static String toMatchedValue(PathContainer path) {
StringBuilder sb = new StringBuilder(path.value().length());
for (PathContainer.Element element : path.elements()) {
if (element instanceof PathContainer.PathSegment segment) {
// the decoded segment, without its matrix parameters
sb.append(segment.valueToMatch());
} else {
// separator
sb.append(element.value());
}
}
return sb.toString();
}

@Override
protected void populateAttachments(HttpServletRequest request, Message message) {
// check if there is multipart files, if so will put it into DataHandler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.component.platform.http.springboot;

import java.util.concurrent.TimeUnit;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.platform.http.PlatformHttpComponent;
import org.apache.camel.component.platform.http.PlatformHttpConstants;
import org.apache.camel.impl.DefaultCamelContext;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* The consumer services the request on its own executor, so the path variables must be resolved without relying on the
* request path the dispatch cached, which may already have been removed by then.
*/
public class SpringBootPlatformHttpBindingPathVariableTest {

@Test
void pathVariablesAreResolvedWithoutACachedRequestPath() throws Exception {
try (DefaultCamelContext context = new DefaultCamelContext()) {
context.getRegistry().bind(PlatformHttpConstants.PLATFORM_HTTP_ENGINE_NAME,
new SpringBootPlatformHttpEngine(8080));
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
from("platform-http:/greeting/{name}")
.transform().simple("${header.name}|${header.CamelHttpPath}");
}
});
context.start();

PlatformHttpComponent component = context.getComponent("platform-http", PlatformHttpComponent.class);
SpringBootPlatformHttpConsumer consumer
= (SpringBootPlatformHttpConsumer) component.getHttpEndpoints().iterator().next().getConsumer();

MockHttpServletRequest request = new MockHttpServletRequest("GET", "/greeting/%61dmin;v=1");
MockHttpServletResponse response = new MockHttpServletResponse();
consumer.service(request, response).get(20, TimeUnit.SECONDS);

assertEquals(200, response.getStatus());
// the placeholder is decoded and carries no matrix parameter, CamelHttpPath stays raw
assertEquals("admin|/greeting/%61dmin;v=1", response.getContentAsString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.component.platform.http.springboot;

import io.restassured.RestAssured;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.StrictHttpFirewall;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

/**
* Path variables must be taken from the path Spring matched the request against, so a percent encoded segment or a
* segment carrying matrix parameters is reported decoded and without its matrix parameters, as the vertx engine does.
*/
@EnableAutoConfiguration
@CamelSpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class,
SpringBootPlatformHttpPathVariableTest.class,
SpringBootPlatformHttpPathVariableTest.TestConfiguration.class,
PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class })
public class SpringBootPlatformHttpPathVariableTest {

@Autowired
private Environment env;

@BeforeEach
void setUp() {
RestAssured.port = env.getRequiredProperty("local.server.port", Integer.class);
}

@Configuration
public static class TestConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(csrf -> csrf.disable());
return http.build();
}

@Bean
public WebSecurityCustomizer allowMatrixParametersCustomizer() {
// the strict firewall rejects matrix parameters by default
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowSemicolon(true);
return web -> web.httpFirewall(firewall);
}

@Bean
public RouteBuilder pathVariableRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("platform-http:/greeting/{name}")
.transform().simple("${header.name}|${header.CamelHttpPath}");

rest("/rest")
.get("/{name}").to("direct:restName");

from("direct:restName")
.setBody().simple("${header.name}");
}
};
}
}

@Test
public void testPlainPathVariable() {
given()
.when()
.get("/greeting/Camel")
.then()
.statusCode(200)
.body(equalTo("Camel|/greeting/Camel"));
}

@Test
public void testPercentEncodedPathVariable() {
// Spring matched /greeting/admin, so the header must be admin, while CamelHttpPath stays the raw path
given()
.urlEncodingEnabled(false)
.when()
.get("/greeting/%61dmin")
.then()
.statusCode(200)
.body(equalTo("admin|/greeting/%61dmin"));
}

@Test
public void testPercentEncodedSpaceInPathVariable() {
given()
.urlEncodingEnabled(false)
.when()
.get("/greeting/John%20Doe")
.then()
.statusCode(200)
.body(equalTo("John Doe|/greeting/John%20Doe"));
}

@Test
public void testMatrixParameterInPathVariable() {
// Spring matched /greeting/name, the matrix parameter is not part of the segment it matched
given()
.urlEncodingEnabled(false)
.when()
.get("/greeting/name;v=1")
.then()
.statusCode(200)
.body(equalTo("name|/greeting/name;v=1"));
}

@Test
public void testRestDslPercentEncodedPathVariable() {
given()
.urlEncodingEnabled(false)
.when()
.get("/rest/%61dmin")
.then()
.statusCode(200)
.body(equalTo("admin"));
}
}
Loading