This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9ee1e7cfade6 CAMEL-26164: camel-whatsapp - fix webhook query parsing
and a missing HTTP method (#28940)
9ee1e7cfade6 is described below
commit 9ee1e7cfade69c1dea913167643a97249007ee74
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:50:26 2026 +0100
CAMEL-26164: camel-whatsapp - fix webhook query parsing and a missing HTTP
method (#28940)
… HTTP method
The webhook verification request split each query pair on = and read the
second part without a check, so an empty value or a pair without = failed
with ArrayIndexOutOfBoundsException. Values were not URL-decoded, so an
encoded hub.verify_token did not match and an encoded hub.challenge was
echoed back encoded. The query is now parsed with indexOf, decoded once
(from CamelHttpRawQuery when present, as netty-http sets CamelHttpQuery
already decoded), and an invalid encoding gives 400.
A request without the CamelHttpMethod header failed with a
NullPointerException; it is now handled as an event (POST), and an event
without a body is read as an empty body.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../whatsapp/WhatsAppWebhookProcessor.java | 44 ++++--
.../whatsapp/WhatsAppWebhookProcessorTest.java | 163 +++++++++++++++++++++
.../component/whatsapp/WhatsAppWebhookTest.java | 16 ++
3 files changed, 213 insertions(+), 10 deletions(-)
diff --git
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
index a120824636cf..0a8bbe0e7e57 100644
---
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
+++
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.whatsapp;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
@@ -58,9 +59,17 @@ public class WhatsAppWebhookProcessor extends
AsyncProcessorSupport {
public boolean process(Exchange exchange, AsyncCallback callback) {
String content;
- if
("GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD).toString()))
{
+ if
("GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD,
String.class))) {
// Parse params from the webhook verification request
- Map<String, String> queryParams = parseQueryParam(exchange);
+ Map<String, String> queryParams;
+ try {
+ queryParams = parseQueryParam(exchange);
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Rejecting WhatsApp webhook verification request with
an invalid query: {}", e.getMessage());
+ exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
400);
+ callback.done(true);
+ return true;
+ }
String mode = queryParams.get(MODE_QUERY_PARAM);
String token = queryParams.get(VERIFY_TOKEN_QUERY_PARAM);
@@ -82,7 +91,7 @@ public class WhatsAppWebhookProcessor extends
AsyncProcessorSupport {
} else {
byte[] requestBody;
try (InputStream body =
exchange.getIn().getBody(InputStream.class)) {
- requestBody = body.readAllBytes();
+ requestBody = body != null ? body.readAllBytes() : new byte[0];
} catch (IOException e) {
exchange.setException(e);
callback.done(true);
@@ -131,15 +140,30 @@ public class WhatsAppWebhookProcessor extends
AsyncProcessorSupport {
}
}
- private Map<String, String> parseQueryParam(Exchange exchange) {
+ /**
+ * Parses the query parameters of the webhook verification request. The
raw query is preferred (netty-http provides
+ * the decoded query in {@link Exchange#HTTP_QUERY}), and every key and
value is URL decoded once. A parameter with
+ * an empty value or without {@code =} gets an empty value.
+ *
+ * @throws IllegalArgumentException if the query has an invalid URL
encoding
+ */
+ static Map<String, String> parseQueryParam(Exchange exchange) {
Map<String, String> queryParams = new HashMap<>();
- if (exchange.getIn().getHeader(Exchange.HTTP_QUERY) != null) {
- String[] pairs =
exchange.getIn().getHeader(Exchange.HTTP_QUERY).toString().split("&");
- for (String pair : pairs) {
- String[] keyValuePair = pair.split("=");
-
- queryParams.put(keyValuePair[0], keyValuePair[1]);
+ String query = exchange.getIn().getHeader(Exchange.HTTP_RAW_QUERY,
String.class);
+ if (query == null) {
+ query = exchange.getIn().getHeader(Exchange.HTTP_QUERY,
String.class);
+ }
+ if (query != null) {
+ for (String pair : query.split("&")) {
+ if (pair.isEmpty()) {
+ continue;
+ }
+ int pos = pair.indexOf('=');
+ String key = pos == -1 ? pair : pair.substring(0, pos);
+ String value = pos == -1 ? "" : pair.substring(pos + 1);
+ queryParams.put(URLDecoder.decode(key, StandardCharsets.UTF_8),
+ URLDecoder.decode(value, StandardCharsets.UTF_8));
}
}
diff --git
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
new file mode 100644
index 000000000000..0ca0c7a1acd9
--- /dev/null
+++
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.whatsapp;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class WhatsAppWebhookProcessorTest {
+
+ private static final String VERIFY_TOKEN = "my token&more";
+
+ private CamelContext context;
+ private WhatsAppWebhookProcessor processor;
+ private final AtomicInteger calls = new AtomicInteger();
+
+ @BeforeEach
+ void setUp() {
+ context = new DefaultCamelContext();
+ WhatsAppConfiguration configuration = new WhatsAppConfiguration();
+ configuration.setWebhookVerifyToken(VERIFY_TOKEN);
+ processor = new WhatsAppWebhookProcessor(exchange ->
calls.incrementAndGet(), configuration);
+ context.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ context.close();
+ }
+
+ @Test
+ void emptyQueryValueAndMissingEqualsSign() {
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
"hub.challenge=&hub.mode=subscribe&flag");
+
+ assertThat(WhatsAppWebhookProcessor.parseQueryParam(exchange))
+ .containsExactlyInAnyOrderEntriesOf(Map.of("hub.challenge",
"", "hub.mode", "subscribe", "flag", ""));
+
+ process(exchange);
+ // hub.verify_token is missing
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+ }
+
+ @Test
+ void verifiedWithEmptyChallenge() {
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=");
+
+ process(exchange);
+
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertThat(exchange.getMessage().getBody()).isEqualTo("");
+ assertThat(calls).hasValue(1);
+ }
+
+ @Test
+ void encodedTokenAndChallengeAreDecoded() {
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+
"hub.mode=subscribe&hub.verify_token=my+token%26more&hub.challenge=a%3Db%20c");
+
+ process(exchange);
+
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertThat(exchange.getMessage().getBody()).isEqualTo("a=b c");
+ assertThat(calls).hasValue(1);
+ }
+
+ @Test
+ void rawQueryIsPreferred() {
+ // netty-http sets the decoded query in CamelHttpQuery and the raw
query in CamelHttpRawQuery
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+ "hub.mode=subscribe&hub.verify_token=my
token&more&hub.challenge=100%");
+ exchange.getIn().setHeader(Exchange.HTTP_RAW_QUERY,
+
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=100%25");
+
+ process(exchange);
+
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertThat(exchange.getMessage().getBody()).isEqualTo("100%");
+ assertThat(calls).hasValue(1);
+ }
+
+ @Test
+ void wrongTokenIsRejected() {
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+
"hub.mode=subscribe&hub.verify_token=my%20token&hub.challenge=123");
+
+ process(exchange);
+
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+ assertThat(exchange.getMessage().getBody()).isNull();
+ }
+
+ @Test
+ void invalidEncodingIsBadRequest() {
+ Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+ "hub.mode=subscribe&hub.verify_token=%zz&hub.challenge=123");
+
+ process(exchange);
+
+
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+ assertThat(exchange.getException()).isNull();
+ assertThat(calls).hasValue(0);
+ }
+
+ @Test
+ void missingHttpMethodIsAnEvent() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody("{\"object\":\"whatsapp_business_account\"}");
+
+ process(exchange);
+
+ assertThat(exchange.getException()).isNull();
+
assertThat(exchange.getMessage().getBody()).isEqualTo("{\"object\":\"whatsapp_business_account\"}");
+ assertThat(calls).hasValue(1);
+ }
+
+ @Test
+ void eventWithoutBody() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Exchange.HTTP_METHOD, "POST");
+
+ process(exchange);
+
+ assertThat(exchange.getException()).isNull();
+ assertThat(exchange.getMessage().getBody()).isEqualTo("");
+ assertThat(calls).hasValue(1);
+ }
+
+ private Exchange verificationRequest(String header, String query) {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Exchange.HTTP_METHOD, "GET");
+ exchange.getIn().setHeader(header, query);
+ return exchange;
+ }
+
+ private void process(Exchange exchange) {
+ processor.process(exchange, doneSync -> {
+ });
+ }
+}
diff --git
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
index 7b34a56d8b36..564df7cb7668 100644
---
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
+++
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.whatsapp;
import java.io.InputStream;
+import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.RoutesBuilder;
@@ -50,6 +51,21 @@ public class WhatsAppWebhookTest extends WhatsAppTestSupport
{
mock.assertIsSatisfied();
}
+ @Test
+ public void testWebhookRegistrationEncodedQuery() throws Exception {
+ WebhookConfiguration config
+ = ((WebhookEndpoint)
context().getRoute("webhook").getConsumer().getEndpoint()).getConfiguration();
+ // a parameter with an empty value, and an encoded challenge
+ String query = "empty=&hub.mode=subscribe&hub.verify_token=" +
VERIFY_TOKEN + "&hub.challenge=a%26b";
+
+ MockEndpoint mock = getMockEndpoint("mock:endpoint");
+ mock.expectedBodiesReceived("a&b");
+
+ template().sendBodyAndHeaders("netty-http:" +
config.computeFullExternalUrl(), null,
+ Map.of(Exchange.HTTP_METHOD, "GET", Exchange.HTTP_QUERY,
query));
+ mock.assertIsSatisfied();
+ }
+
@Test
public void testWebhookRegistrationFailed() throws Exception {
String challenge = "987";