This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karaf.git
The following commit(s) were added to refs/heads/main by this push:
new 94a99b4e4 fix(#730): keep control characters out of shell endpoint uri
output (#735)
94a99b4e4 is described below
commit 94a99b4e4e7672c8d2e4dad0b8bbfbfa4bef7fd4
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Aug 25 09:38:19 2026 +0200
fix(#730): keep control characters out of shell endpoint uri output (#735)
camel:endpoint-list, camel:endpoint-stats and camel:rest-registry-list all
url decode the endpoint uri before printing it, and --decode defaults to
true. URISupport.sanitizeUri masks credentials but leaves control
characters alone, so percent encoded ESC, LF or BEL were re-armed by the
decode and passed to ShellTable as raw bytes.
The uris are not all route-author literals: getEndpoints() includes
endpoints materialized by the dynamic EIPs and RuntimeEndpointRegistry
tracks dynamic endpoints by design, so a uri can carry a substring that
arrived on the wire. A raw LF splits the row, and ESC sequences are acted
on by the terminal emulator, in the exact view an operator uses to see
what a context is connected to.
Fold the shared decode/sanitize into ShellUriHelper.prepareUriForDisplay
and put control characters back into percent encoded form as the last
step. Nothing is lost from the display: the operator sees %1B where the
uri really has an ESC, and readability of ordinary escapes is unchanged.
Also applies when --decode is false, so a literal control character in a
uri is handled too.
While here, an undecodable uri no longer aborts the whole listing: a
malformed % sequence made URLDecoder throw and took out every row, not
just the offending one.
Adds ShellUriHelperTest and the junit-jupiter test dependencies the shell
module did not have yet.
---
shell/pom.xml | 20 ++++
.../org/apache/camel/karaf/shell/EndpointList.java | 10 +-
.../apache/camel/karaf/shell/EndpointStats.java | 9 +-
.../apache/camel/karaf/shell/RestRegistryList.java | 10 +-
.../apache/camel/karaf/shell/ShellUriHelper.java | 102 ++++++++++++++++++++
.../camel/karaf/shell/ShellUriHelperTest.java | 104 +++++++++++++++++++++
6 files changed, 229 insertions(+), 26 deletions(-)
diff --git a/shell/pom.xml b/shell/pom.xml
index fa0cb0357..9bff7bbbb 100644
--- a/shell/pom.xml
+++ b/shell/pom.xml
@@ -46,6 +46,26 @@
<artifactId>camel-core</artifactId>
<version>${camel-version}</version>
</dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>${junit-jupiter-version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>${junit-jupiter-version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <version>${junit-jupiter-version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/shell/src/main/java/org/apache/camel/karaf/shell/EndpointList.java
b/shell/src/main/java/org/apache/camel/karaf/shell/EndpointList.java
index ff8a57c3b..77a510493 100644
--- a/shell/src/main/java/org/apache/camel/karaf/shell/EndpointList.java
+++ b/shell/src/main/java/org/apache/camel/karaf/shell/EndpointList.java
@@ -21,12 +21,10 @@ import org.apache.camel.Endpoint;
import org.apache.camel.ServiceStatus;
import org.apache.camel.StatefulService;
import org.apache.camel.karaf.shell.completers.CamelContextCompleter;
-import org.apache.camel.util.URISupport;
import org.apache.karaf.shell.api.action.*;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.apache.karaf.shell.support.table.ShellTable;
-import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -63,13 +61,7 @@ public class EndpointList extends CamelCommandSupport
implements Action {
}
});
for (Endpoint endpoint : endpoints) {
- String uri = endpoint.getEndpointUri();
- if (decode) {
- // decode uri so its more human readable
- uri = URLDecoder.decode(uri, "UTF-8");
- }
- // sanitize and mask uri so we don't see passwords
- uri = URISupport.sanitizeUri(uri);
+ String uri =
ShellUriHelper.prepareUriForDisplay(endpoint.getEndpointUri(), decode);
table.addRow().addContent(camelContext.getName(), uri,
getEndpointState(endpoint));
}
}
diff --git
a/shell/src/main/java/org/apache/camel/karaf/shell/EndpointStats.java
b/shell/src/main/java/org/apache/camel/karaf/shell/EndpointStats.java
index 5b6fbb805..0a420c3c1 100644
--- a/shell/src/main/java/org/apache/camel/karaf/shell/EndpointStats.java
+++ b/shell/src/main/java/org/apache/camel/karaf/shell/EndpointStats.java
@@ -20,12 +20,10 @@ import org.apache.camel.CamelContext;
import org.apache.camel.karaf.shell.completers.CamelContextCompleter;
import org.apache.camel.spi.EndpointRegistry;
import org.apache.camel.spi.RuntimeEndpointRegistry;
-import org.apache.camel.util.URISupport;
import org.apache.karaf.shell.api.action.*;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.apache.karaf.shell.support.table.ShellTable;
-import java.net.URLDecoder;
import java.util.List;
@Command(scope = "camel", name = "endpoint-stats", description = "List the
statistics of the Camel endpoints")
@@ -66,12 +64,7 @@ public class EndpointStats extends CamelCommandSupport
implements Action {
boolean isDynamic = endpointRegistry.isDynamic(uri);
long hits = stat.getHits();
- if (decode) {
- // decode uri so it's more human readable
- uri = URLDecoder.decode(uri, "UTF-8");
- }
- // sanitize and mask uri so we don't see passwords
- uri = URISupport.sanitizeUri(uri);
+ uri = ShellUriHelper.prepareUriForDisplay(uri, decode);
// should we filter ?
if (isValidRow(direction, Boolean.toString(isStatic),
Boolean.toString(isDynamic))) {
diff --git
a/shell/src/main/java/org/apache/camel/karaf/shell/RestRegistryList.java
b/shell/src/main/java/org/apache/camel/karaf/shell/RestRegistryList.java
index b1beffdb3..a1335dc5d 100644
--- a/shell/src/main/java/org/apache/camel/karaf/shell/RestRegistryList.java
+++ b/shell/src/main/java/org/apache/camel/karaf/shell/RestRegistryList.java
@@ -19,12 +19,10 @@ package org.apache.camel.karaf.shell;
import org.apache.camel.CamelContext;
import org.apache.camel.karaf.shell.completers.CamelContextCompleter;
import org.apache.camel.spi.RestRegistry;
-import org.apache.camel.util.URISupport;
import org.apache.karaf.shell.api.action.*;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.apache.karaf.shell.support.table.ShellTable;
-import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -64,13 +62,7 @@ public class RestRegistryList extends CamelCommandSupport
implements Action {
}
});
for (RestRegistry.RestService service : services) {
- String uri = service.getUrl();
- if (decode) {
- // decode uri so it's more human readable
- uri = URLDecoder.decode(uri, "UTF-8");
- }
- // sanitize and mask uri so we don't see passwords
- uri = URISupport.sanitizeUri(uri);
+ String uri = ShellUriHelper.prepareUriForDisplay(service.getUrl(),
decode);
table.addRow().addContent(uri,
service.getBasePath(),
service.getUriTemplate(),
diff --git
a/shell/src/main/java/org/apache/camel/karaf/shell/ShellUriHelper.java
b/shell/src/main/java/org/apache/camel/karaf/shell/ShellUriHelper.java
new file mode 100644
index 000000000..34f466870
--- /dev/null
+++ b/shell/src/main/java/org/apache/camel/karaf/shell/ShellUriHelper.java
@@ -0,0 +1,102 @@
+/*
+ * 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.karaf.shell;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.util.URISupport;
+
+/**
+ * Helper to prepare endpoint uris for display by the Camel shell commands.
+ */
+public final class ShellUriHelper {
+
+ private ShellUriHelper() {
+ }
+
+ /**
+ * Prepares an endpoint uri so it can be safely written to the console.
+ * <p/>
+ * The uri is optionally decoded so it is human readable, any credentials
are masked, and control characters are
+ * left in (or put back into) their percent encoded form. Endpoint uris
are not always authored in the route: the
+ * dynamic EIPs materialize endpoints whose uri can embed message content,
so a decoded uri may carry characters
+ * the terminal would otherwise act on.
+ *
+ * @param uri the endpoint uri, may be <tt>null</tt>
+ * @param decode whether to url decode the uri so it is more human
readable
+ * @return the uri to display, or <tt>null</tt> if the given uri
was <tt>null</tt>
+ */
+ public static String prepareUriForDisplay(String uri, boolean decode) {
+ if (uri == null) {
+ return null;
+ }
+ if (decode) {
+ // decode uri so its more human readable
+ uri = decodeQuietly(uri);
+ }
+ // sanitize and mask uri so we don't see passwords
+ uri = URISupport.sanitizeUri(uri);
+ // must be done last so nothing can put a control character back
afterwards
+ return encodeControlCharacters(uri);
+ }
+
+ /**
+ * Url decodes the uri, returning it unchanged when it is not decodable,
so that a single malformed escape does not
+ * fail the whole command.
+ */
+ private static String decodeQuietly(String uri) {
+ try {
+ return URLDecoder.decode(uri, StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ return uri;
+ }
+ }
+
+ /**
+ * Puts any control character back into its percent encoded form. A uri
has no need for control characters, and
+ * decoding them would let them reach the terminal, where they can move
the cursor, forge additional rows in the
+ * output, or be interpreted as an escape sequence.
+ */
+ private static String encodeControlCharacters(String uri) {
+ int first = indexOfControlCharacter(uri, 0);
+ if (first == -1) {
+ // by far the common case, so do not build anything
+ return uri;
+ }
+ StringBuilder sb = new StringBuilder(uri.length() + 16);
+ sb.append(uri, 0, first);
+ for (int i = first; i < uri.length(); i++) {
+ char ch = uri.charAt(i);
+ if (Character.isISOControl(ch)) {
+ sb.append('%').append(String.format("%02X", (int) ch));
+ } else {
+ sb.append(ch);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static int indexOfControlCharacter(String uri, int from) {
+ for (int i = from; i < uri.length(); i++) {
+ if (Character.isISOControl(uri.charAt(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git
a/shell/src/test/java/org/apache/camel/karaf/shell/ShellUriHelperTest.java
b/shell/src/test/java/org/apache/camel/karaf/shell/ShellUriHelperTest.java
new file mode 100644
index 000000000..890456c31
--- /dev/null
+++ b/shell/src/test/java/org/apache/camel/karaf/shell/ShellUriHelperTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.karaf.shell;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ShellUriHelperTest {
+
+ private static final char ESC = 0x1B;
+
+ @Test
+ public void testNullUri() {
+ assertNull(ShellUriHelper.prepareUriForDisplay(null, true));
+ assertNull(ShellUriHelper.prepareUriForDisplay(null, false));
+ }
+
+ @Test
+ public void testPlainUriIsUnchanged() {
+ String uri = "http://localhost:8080/push";
+ assertEquals(uri, ShellUriHelper.prepareUriForDisplay(uri, true));
+ assertEquals(uri, ShellUriHelper.prepareUriForDisplay(uri, false));
+ }
+
+ @Test
+ public void testDecodeStillMakesUriReadable() {
+ assertEquals("timer://foo?period=5000&message=hello world",
+
ShellUriHelper.prepareUriForDisplay("timer://foo?period=5000&message=hello%20world",
true));
+ }
+
+ @Test
+ public void testNoDecodeLeavesUriEncoded() {
+ assertEquals("timer://foo?message=hello%20world",
+
ShellUriHelper.prepareUriForDisplay("timer://foo?message=hello%20world",
false));
+ }
+
+ @Test
+ public void testCredentialsAreStillMasked() {
+ String uri =
ShellUriHelper.prepareUriForDisplay("ftp://host/dir?username=scott&password=tiger&binary=true",
true);
+ assertFalse(uri.contains("tiger"), "password must be masked, was: " +
uri);
+ assertFalse(uri.contains("scott"), "username must be masked, was: " +
uri);
+ assertTrue(uri.contains("binary=true"), "non credential options are
kept, was: " + uri);
+ assertTrue(uri.startsWith("ftp://host/dir?"), "the endpoint itself is
still readable, was: " + uri);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"%1b", "%1B", "%0a", "%0d", "%07", "%00", "%7f"})
+ public void testEncodedControlCharacterIsNotReArmedByDecode(String
encoded) {
+ String uri = ShellUriHelper.prepareUriForDisplay("http://host/" +
encoded + "evil", true);
+ assertFalse(containsControlCharacter(uri), "decoded uri must not
contain a control character, was: " + uri);
+ }
+
+ @Test
+ public void testEscapeSequenceIsRenderedInline() {
+ // %1b decodes to ESC, which must come back as %1B rather than
reaching the terminal
+ assertEquals("http://host/%1B]2;title%07",
+
ShellUriHelper.prepareUriForDisplay("http://host/%1b]2;title%07", true));
+ }
+
+ @Test
+ public void testNewlineCannotForgeAnExtraRow() {
+ String uri =
ShellUriHelper.prepareUriForDisplay("http://evil/%0aseda://looks-legit", true);
+ assertFalse(uri.contains("\n"), "a decoded newline must not split the
row, was: " + uri);
+ assertEquals("http://evil/%0Aseda://looks-legit", uri);
+ }
+
+ @Test
+ public void testLiteralControlCharacterIsEncodedEvenWithoutDecode() {
+ String uri = ShellUriHelper.prepareUriForDisplay("http://host/" + ESC
+ "[2J", false);
+ assertFalse(containsControlCharacter(uri), "was: " + uri);
+ assertEquals("http://host/%1B[2J", uri);
+ }
+
+ @Test
+ public void testMalformedEscapeDoesNotFailTheCommand() {
+ // an incomplete % sequence makes URLDecoder throw, which used to
abort the whole listing
+ assertEquals("http://host/100%",
ShellUriHelper.prepareUriForDisplay("http://host/100%", true));
+ assertEquals("http://host/%zz",
ShellUriHelper.prepareUriForDisplay("http://host/%zz", true));
+ }
+
+ private static boolean containsControlCharacter(String s) {
+ return s.chars().anyMatch(Character::isISOControl);
+ }
+}