This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch camel-2.x in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-2.x by this push: new 48ad52a CAMEL-12982: Support RAW{} syntax in URISupport (#2717) 48ad52a is described below commit 48ad52ae723a9decdd6469511defed4c05672e58 Author: Tadayoshi Sato <sato.tadayo...@gmail.com> AuthorDate: Fri Jan 18 23:43:30 2019 +0900 CAMEL-12982: Support RAW{} syntax in URISupport (#2717) --- .../org/apache/camel/impl/DefaultComponent.java | 6 +- .../apache/camel/model/ToDynamicDefinition.java | 47 +--- .../java/org/apache/camel/runtimecatalog/Pair.java | 60 +++++ .../apache/camel/runtimecatalog/URISupport.java | 121 +++++++-- .../runtimecatalog/UnsafeUriCharactersEncoder.java | 48 +--- .../src/main/java/org/apache/camel/util/Pair.java | 60 +++++ .../java/org/apache/camel/util/URIScanner.java | 270 +++++++++++++++++++++ .../java/org/apache/camel/util/URISupport.java | 258 +++++++------------- .../camel/util/UnsafeUriCharactersEncoder.java | 46 +--- .../issues/EndpointWithRawUriParameterTest.java | 15 ++ .../java/org/apache/camel/util/URISupportTest.java | 182 ++++++++++---- platforms/camel-catalog/pom.xml | 2 + .../main/java/org/apache/camel/catalog/Pair.java | 60 +++++ .../java/org/apache/camel/catalog/URISupport.java | 121 +++++++-- .../camel/catalog/UnsafeUriCharactersEncoder.java | 48 +--- 15 files changed, 916 insertions(+), 428 deletions(-) diff --git a/camel-core/src/main/java/org/apache/camel/impl/DefaultComponent.java b/camel-core/src/main/java/org/apache/camel/impl/DefaultComponent.java index bcdbd95..f453299 100644 --- a/camel-core/src/main/java/org/apache/camel/impl/DefaultComponent.java +++ b/camel-core/src/main/java/org/apache/camel/impl/DefaultComponent.java @@ -53,7 +53,11 @@ import org.slf4j.LoggerFactory; */ public abstract class DefaultComponent extends ServiceSupport implements Component { private static final Logger LOG = LoggerFactory.getLogger(DefaultComponent.class); - private static final Pattern RAW_PATTERN = Pattern.compile("RAW(.*&&.*)"); + + /** + * Simple RAW() pattern used only for validating URI in this class + */ + private static final Pattern RAW_PATTERN = Pattern.compile("RAW[({].*&&.*[)}]"); private final List<Supplier<ComponentExtension>> extensions = new ArrayList<>(); diff --git a/camel-core/src/main/java/org/apache/camel/model/ToDynamicDefinition.java b/camel-core/src/main/java/org/apache/camel/model/ToDynamicDefinition.java index 35065fe..98bb675 100644 --- a/camel-core/src/main/java/org/apache/camel/model/ToDynamicDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/ToDynamicDefinition.java @@ -18,9 +18,6 @@ package org.apache.camel.model; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; @@ -35,7 +32,9 @@ import org.apache.camel.processor.SendDynamicProcessor; import org.apache.camel.spi.Language; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.RouteContext; +import org.apache.camel.util.Pair; import org.apache.camel.util.StringHelper; +import org.apache.camel.util.URISupport; /** * Sends the message to a dynamic endpoint @@ -51,8 +50,6 @@ import org.apache.camel.util.StringHelper; @XmlAccessorType(XmlAccessType.FIELD) public class ToDynamicDefinition extends NoOutputDefinition<ToDynamicDefinition> { - private static final Pattern RAW_PATTERN = Pattern.compile("RAW\\([^\\)]+\\)"); - @XmlAttribute @Metadata(required = "true") private String uri; @XmlAttribute @@ -230,42 +227,6 @@ public class ToDynamicDefinition extends NoOutputDefinition<ToDynamicDefinition> // Utilities // ------------------------------------------------------------------------- - private static class Pair { - int left; - int right; - Pair(int left, int right) { - this.left = left; - this.right = right; - } - } - - private static List<Pair> checkRAW(String s) { - Matcher matcher = RAW_PATTERN.matcher(s); - List<Pair> answer = new ArrayList<>(); - // Check all occurrences - while (matcher.find()) { - answer.add(new Pair(matcher.start(), matcher.end() - 1)); - } - return answer; - } - - private static boolean isRaw(int index, List<Pair>pairs) { - for (Pair pair : pairs) { - if (index < pair.left) { - return false; - } else { - if (index >= pair.left) { - if (index <= pair.right) { - return true; - } else { - continue; - } - } - } - } - return false; - } - /** * We need to split the string safely for each + sign, but avoid splitting within RAW(...). */ @@ -277,12 +238,12 @@ public class ToDynamicDefinition extends NoOutputDefinition<ToDynamicDefinition> list.add(s); } else { // there is a plus sign so we need to split in a safe manner - List<Pair> rawPairs = checkRAW(s); + List<Pair<Integer>> rawPairs = URISupport.scanRaw(s); StringBuilder sb = new StringBuilder(); char chars[] = s.toCharArray(); for (int i = 0; i < chars.length; i++) { char ch = chars[i]; - if (ch != '+' || isRaw(i, rawPairs)) { + if (ch != '+' || URISupport.isRaw(i, rawPairs)) { sb.append(ch); } else { list.add(sb.toString()); diff --git a/camel-core/src/main/java/org/apache/camel/runtimecatalog/Pair.java b/camel-core/src/main/java/org/apache/camel/runtimecatalog/Pair.java new file mode 100644 index 0000000..99d7b9c --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/runtimecatalog/Pair.java @@ -0,0 +1,60 @@ +/** + * 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.runtimecatalog; + +import java.util.Objects; + +/** + * Copied from org.apache.camel.util.Pair + */ +public class Pair<T> { + + private T left; + private T right; + + public Pair(T left, T right) { + this.left = left; + this.right = right; + } + + public T getLeft() { + return left; + } + + public T getRight() { + return right; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair<?> that = (Pair<?>) o; + return Objects.equals(left, that.left) && + Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } + + @Override + public String toString() { + return "(" + left + ", " + right + ")"; + } +} diff --git a/camel-core/src/main/java/org/apache/camel/runtimecatalog/URISupport.java b/camel-core/src/main/java/org/apache/camel/runtimecatalog/URISupport.java index 8bd0814..bc11ba2 100644 --- a/camel-core/src/main/java/org/apache/camel/runtimecatalog/URISupport.java +++ b/camel-core/src/main/java/org/apache/camel/runtimecatalog/URISupport.java @@ -26,14 +26,18 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; + +import org.apache.camel.runtimecatalog.Pair; /** * Copied from org.apache.camel.util.URISupport */ public final class URISupport { - public static final String RAW_TOKEN_START = "RAW("; - public static final String RAW_TOKEN_END = ")"; + public static final String RAW_TOKEN_PREFIX = "RAW"; + public static final char[] RAW_TOKEN_START = { '(', '{' }; + public static final char[] RAW_TOKEN_END = { ')', '}' }; private static final String CHARSET = "UTF-8"; @@ -155,17 +159,17 @@ public final class URISupport { * @see #RAW_TOKEN_END */ public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { - // must check for trailing & as the uri.split("&") will ignore those - if (uri != null && uri.endsWith("&")) { - throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " - + "Check the uri and remove the trailing & marker."); - } - if (isEmpty(uri)) { // return an empty map return new LinkedHashMap<>(0); } + // must check for trailing & as the uri.split("&") will ignore those + if (uri.endsWith("&")) { + throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + + "Check the uri and remove the trailing & marker."); + } + // need to parse the uri query parameters manually as we cannot rely on splitting by &, // as & can be used in a parameter value as well. @@ -192,7 +196,15 @@ public final class URISupport { } // are we a raw value - isRaw = value.toString().startsWith(RAW_TOKEN_START); + char rawTokenEnd = 0; + for (int j = 0; j < RAW_TOKEN_START.length; j++) { + String rawTokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[j]; + isRaw = value.toString().startsWith(rawTokenStart); + if (isRaw) { + rawTokenEnd = RAW_TOKEN_END[j]; + break; + } + } // if we are in raw mode, then we keep adding until we hit the end marker if (isRaw) { @@ -202,9 +214,9 @@ public final class URISupport { value.append(ch); } - // we only end the raw marker if its )& or at the end of the value + // we only end the raw marker if it's ")&", "}&", or at the end of the value - boolean end = ch == RAW_TOKEN_END.charAt(0) && (next == '&' || next == '\u0000'); + boolean end = ch == rawTokenEnd && (next == '&' || next == '\u0000'); if (end) { // raw value end, so add that as a parameter, and reset flags addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); @@ -302,6 +314,71 @@ public final class URISupport { } } + public static List<Pair<Integer>> scanRaw(String str) { + List<Pair<Integer>> answer = new ArrayList<>(); + if (str == null || isEmpty(str)) { + return answer; + } + + int offset = 0; + int start = str.indexOf(RAW_TOKEN_PREFIX); + while (start >= 0 && offset < str.length()) { + offset = start + RAW_TOKEN_PREFIX.length(); + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + char tokenEnd = RAW_TOKEN_END[i]; + if (str.startsWith(tokenStart, start)) { + offset = scanRawToEnd(str, start, tokenStart, tokenEnd, answer); + continue; + } + } + start = str.indexOf(RAW_TOKEN_PREFIX, offset); + } + return answer; + } + + private static int scanRawToEnd(String str, int start, String tokenStart, char tokenEnd, + List<Pair<Integer>> answer) { + // we search the first end bracket to close the RAW token + // as opposed to parsing query, this doesn't allow the occurrences of end brackets + // inbetween because this may be used on the host/path parts of URI + // and thus we cannot rely on '&' for detecting the end of a RAW token + int end = str.indexOf(tokenEnd, start + tokenStart.length()); + if (end < 0) { + // still return a pair even if RAW token is not closed + answer.add(new Pair<>(start, str.length())); + return str.length(); + } + answer.add(new Pair<>(start, end)); + return end + 1; + } + + public static boolean isRaw(int index, List<Pair<Integer>> pairs) { + for (Pair<Integer> pair : pairs) { + if (index < pair.getLeft()) { + return false; + } + if (index <= pair.getRight()) { + return true; + } + } + return false; + } + + private static boolean resolveRaw(String str, BiConsumer<String, String> consumer) { + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + String tokenEnd = String.valueOf(RAW_TOKEN_END[i]); + if (str.startsWith(tokenStart) && str.endsWith(tokenEnd)) { + String raw = str.substring(tokenStart.length(), str.length() - 1); + consumer.accept(str, raw); + return true; + } + } + // not RAW value + return false; + } + /** * Assembles a query from the given map. * @@ -346,18 +423,20 @@ public final class URISupport { } else { rc.append(key); } + if (value == null) { + return; + } // only append if value is not null - if (value != null) { - rc.append("="); - if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) { - // do not encode RAW parameters - rc.append(value); + rc.append("="); + boolean isRaw = resolveRaw(value, (str, raw) -> { + // do not encode RAW parameters + rc.append(str); + }); + if (!isRaw) { + if (encode) { + rc.append(URLEncoder.encode(value, CHARSET)); } else { - if (encode) { - rc.append(URLEncoder.encode(value, CHARSET)); - } else { - rc.append(value); - } + rc.append(value); } } } diff --git a/camel-core/src/main/java/org/apache/camel/runtimecatalog/UnsafeUriCharactersEncoder.java b/camel-core/src/main/java/org/apache/camel/runtimecatalog/UnsafeUriCharactersEncoder.java index 2aee59f..acb3838 100644 --- a/camel-core/src/main/java/org/apache/camel/runtimecatalog/UnsafeUriCharactersEncoder.java +++ b/camel-core/src/main/java/org/apache/camel/runtimecatalog/UnsafeUriCharactersEncoder.java @@ -19,8 +19,8 @@ package org.apache.camel.runtimecatalog; import java.util.ArrayList; import java.util.BitSet; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import org.apache.camel.runtimecatalog.Pair; /** * Encoder for unsafe URI characters. @@ -32,7 +32,6 @@ public final class UnsafeUriCharactersEncoder { private static BitSet unsafeCharactersHttp; private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'}; - private static final Pattern RAW_PATTERN = Pattern.compile("RAW\\([^\\)]+\\)"); static { unsafeCharactersRfc1738 = new BitSet(256); @@ -94,48 +93,11 @@ public final class UnsafeUriCharactersEncoder { return encode(s, unsafeCharactersHttp, checkRaw); } - private static List<Pair> checkRAW(String s) { - Matcher matcher = RAW_PATTERN.matcher(s); - List<Pair> answer = new ArrayList<>(); - // Check all occurrences - while (matcher.find()) { - answer.add(new Pair(matcher.start(), matcher.end())); - } - return answer; - } - - private static boolean isRaw(int index, List<Pair> pairs) { - for (Pair pair : pairs) { - if (index < pair.left) { - return false; - } else { - if (index >= pair.left) { - if (index <= pair.right) { - return true; - } else { - continue; - } - } - } - } - return false; - } - - private static class Pair { - int left; - int right; - - Pair(int left, int right) { - this.left = left; - this.right = right; - } - } - // Just skip the encode for isRAW part public static String encode(String s, BitSet unsafeCharacters, boolean checkRaw) { - List<Pair> rawPairs; + List<Pair<Integer>> rawPairs; if (checkRaw) { - rawPairs = checkRAW(s); + rawPairs = URISupport.scanRaw(s); } else { rawPairs = new ArrayList<>(); } @@ -170,7 +132,7 @@ public final class UnsafeUriCharactersEncoder { char next = i + 1 < chars.length ? chars[i + 1] : ' '; char next2 = i + 2 < chars.length ? chars[i + 2] : ' '; - if (isHexDigit(next) && isHexDigit(next2) && !isRaw(i, rawPairs)) { + if (isHexDigit(next) && isHexDigit(next2) && !URISupport.isRaw(i, rawPairs)) { // its already encoded (decimal encoded) so just append as is sb.append(ch); } else { diff --git a/camel-core/src/main/java/org/apache/camel/util/Pair.java b/camel-core/src/main/java/org/apache/camel/util/Pair.java new file mode 100644 index 0000000..8f4e3b4 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/util/Pair.java @@ -0,0 +1,60 @@ +/** + * 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.util; + +import java.util.Objects; + +/** + * Generic holder object for pair values. + */ +public class Pair<T> { + + private T left; + private T right; + + public Pair(T left, T right) { + this.left = left; + this.right = right; + } + + public T getLeft() { + return left; + } + + public T getRight() { + return right; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair<?> that = (Pair<?>) o; + return Objects.equals(left, that.left) && + Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } + + @Override + public String toString() { + return "(" + left + ", " + right + ")"; + } +} diff --git a/camel-core/src/main/java/org/apache/camel/util/URIScanner.java b/camel-core/src/main/java/org/apache/camel/util/URIScanner.java new file mode 100644 index 0000000..7bf813b --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/util/URIScanner.java @@ -0,0 +1,270 @@ +/** + * 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.util; + +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import static org.apache.camel.util.URISupport.RAW_TOKEN_END; +import static org.apache.camel.util.URISupport.RAW_TOKEN_PREFIX; +import static org.apache.camel.util.URISupport.RAW_TOKEN_START; + +/** + * RAW syntax aware URI scanner that provides various URI manipulations. + */ +class URIScanner { + + private enum Mode { + KEY, VALUE + } + + private static final char END = '\u0000'; + + private final String charset; + private final StringBuilder key; + private final StringBuilder value; + private Mode mode; + private boolean isRaw; + private char rawTokenEnd; + + public URIScanner(String charset) { + this.charset = charset; + key = new StringBuilder(); + value = new StringBuilder(); + } + + private void initState() { + mode = Mode.KEY; + key.setLength(0); + value.setLength(0); + isRaw = false; + } + + private String getDecodedKey() throws UnsupportedEncodingException { + return URLDecoder.decode(key.toString(), charset); + } + + private String getDecodedValue() throws UnsupportedEncodingException { + // need to replace % with %25 + String s = StringHelper.replaceAll(value.toString(), "%", "%25"); + String answer = URLDecoder.decode(s, charset); + return answer; + } + + public Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { + // need to parse the uri query parameters manually as we cannot rely on splitting by &, + // as & can be used in a parameter value as well. + + try { + // use a linked map so the parameters is in the same order + Map<String, Object> answer = new LinkedHashMap<>(); + + initState(); + + // parse the uri parameters char by char + for (int i = 0; i < uri.length(); i++) { + // current char + char ch = uri.charAt(i); + // look ahead of the next char + char next; + if (i <= uri.length() - 2) { + next = uri.charAt(i + 1); + } else { + next = END; + } + + switch (mode) { + case KEY: + // if there is a = sign then the key ends and we are in value mode + if (ch == '=') { + mode = Mode.VALUE; + continue; + } + + if (ch != '&') { + // regular char so add it to the key + key.append(ch); + } + break; + case VALUE: + // are we a raw value + isRaw = checkRaw(); + + // if we are in raw mode, then we keep adding until we hit the end marker + if (isRaw) { + value.append(ch); + + if (isAtEnd(ch, next)) { + // raw value end, so add that as a parameter, and reset flags + addParameter(answer, useRaw || isRaw); + initState(); + // skip to next as we are in raw mode and have already added the value + i++; + } + continue; + } + + if (ch != '&') { + // regular char so add it to the value + value.append(ch); + } + break; + default: + throw new IllegalStateException("Unknown mode: " + mode); + } + + // the & denote parameter is ended + if (ch == '&') { + // parameter is ended, as we hit & separator + addParameter(answer, useRaw || isRaw); + initState(); + } + } + + // any left over parameters, then add that + if (key.length() > 0) { + addParameter(answer, useRaw || isRaw); + } + + return answer; + + } catch (UnsupportedEncodingException e) { + URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); + se.initCause(e); + throw se; + } + } + + private boolean checkRaw() { + rawTokenEnd = 0; + + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String rawTokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + boolean isRaw = value.toString().startsWith(rawTokenStart); + if (isRaw) { + rawTokenEnd = RAW_TOKEN_END[i]; + return true; + } + } + + return false; + } + + private boolean isAtEnd(char ch, char next) { + // we only end the raw marker if it's ")&", "}&", or at the end of the value + return ch == rawTokenEnd && (next == '&' || next == END); + } + + private void addParameter(Map<String, Object> answer, boolean isRaw) throws UnsupportedEncodingException { + String name = getDecodedKey(); + String value = isRaw ? this.value.toString() : getDecodedValue(); + + // does the key already exist? + if (answer.containsKey(name)) { + // yes it does, so make sure we can support multiple values, but using a list + // to hold the multiple values + Object existing = answer.get(name); + List<String> list; + if (existing instanceof List) { + list = CastUtils.cast((List<?>) existing); + } else { + // create a new list to hold the multiple values + list = new ArrayList<>(); + String s = existing != null ? existing.toString() : null; + if (s != null) { + list.add(s); + } + } + list.add(value); + answer.put(name, list); + } else { + answer.put(name, value); + } + } + + public static List<Pair<Integer>> scanRaw(String str) { + List<Pair<Integer>> answer = new ArrayList<>(); + if (str == null || ObjectHelper.isEmpty(str)) { + return answer; + } + + int offset = 0; + int start = str.indexOf(RAW_TOKEN_PREFIX); + while (start >= 0 && offset < str.length()) { + offset = start + RAW_TOKEN_PREFIX.length(); + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + char tokenEnd = RAW_TOKEN_END[i]; + if (str.startsWith(tokenStart, start)) { + offset = scanRawToEnd(str, start, tokenStart, tokenEnd, answer); + continue; + } + } + start = str.indexOf(RAW_TOKEN_PREFIX, offset); + } + return answer; + } + + private static int scanRawToEnd(String str, int start, String tokenStart, char tokenEnd, + List<Pair<Integer>> answer) { + // we search the first end bracket to close the RAW token + // as opposed to parsing query, this doesn't allow the occurrences of end brackets + // inbetween because this may be used on the host/path parts of URI + // and thus we cannot rely on '&' for detecting the end of a RAW token + int end = str.indexOf(tokenEnd, start + tokenStart.length()); + if (end < 0) { + // still return a pair even if RAW token is not closed + answer.add(new Pair<>(start, str.length())); + return str.length(); + } + answer.add(new Pair<>(start, end)); + return end + 1; + } + + public static boolean isRaw(int index, List<Pair<Integer>> pairs) { + for (Pair<Integer> pair : pairs) { + if (index < pair.getLeft()) { + return false; + } + if (index <= pair.getRight()) { + return true; + } + } + return false; + } + + public static boolean resolveRaw(String str, BiConsumer<String, String> consumer) { + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + String tokenEnd = String.valueOf(RAW_TOKEN_END[i]); + if (str.startsWith(tokenStart) && str.endsWith(tokenEnd)) { + String raw = str.substring(tokenStart.length(), str.length() - 1); + consumer.accept(str, raw); + return true; + } + } + // not RAW value + return false; + } + +} diff --git a/camel-core/src/main/java/org/apache/camel/util/URISupport.java b/camel-core/src/main/java/org/apache/camel/util/URISupport.java index 1b13164..74424bf 100644 --- a/camel-core/src/main/java/org/apache/camel/util/URISupport.java +++ b/camel-core/src/main/java/org/apache/camel/util/URISupport.java @@ -19,7 +19,6 @@ package org.apache.camel.util; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Iterator; @@ -31,27 +30,29 @@ import java.util.regex.Pattern; /** * URI utilities. * - * @version + * @version */ public final class URISupport { - public static final String RAW_TOKEN_START = "RAW("; - public static final String RAW_TOKEN_END = ")"; + public static final String RAW_TOKEN_PREFIX = "RAW"; + public static final char[] RAW_TOKEN_START = { '(', '{' }; + public static final char[] RAW_TOKEN_END = { ')', '}' }; // Match any key-value pair in the URI query string whose key contains // "passphrase" or "password" or secret key (case-insensitive). // First capture group is the key, second is the value. - private static final Pattern SECRETS = Pattern.compile("([?&][^=]*(?:passphrase|password|secretKey)[^=]*)=(RAW\\(.*\\)|[^&]*)", + private static final Pattern SECRETS = Pattern.compile( + "([?&][^=]*(?:passphrase|password|secretKey)[^=]*)=(RAW[({].*[)}]|[^&]*)", Pattern.CASE_INSENSITIVE); - + // Match the user password in the URI as second capture group // (applies to URI with authority component and userinfo token in the form "user:password"). private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*:)(.*)(@)"); - + // Match the user password in the URI path as second capture group // (applies to URI path with authority component and userinfo token in the form "user:password"). private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*:)(.*)(@)"); - + private static final String CHARSET = "UTF-8"; private URISupport() { @@ -75,12 +76,12 @@ public final class URISupport { } return sanitized; } - + /** * Removes detected sensitive information (such as passwords) from the * <em>path part</em> of an URI (that is, the part without the query * parameters or component prefix) and returns the result. - * + * * @param path the URI path to sanitize * @return null if the path is null, otherwise the sanitized path */ @@ -124,6 +125,7 @@ public final class URISupport { * @param uri the uri * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. + * @see #RAW_TOKEN_PREFIX * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ @@ -142,6 +144,7 @@ public final class URISupport { * @param useRaw whether to force using raw values * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. + * @see #RAW_TOKEN_PREFIX * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ @@ -161,147 +164,61 @@ public final class URISupport { * @param lenient whether to parse lenient and ignore trailing & markers which has no key or value which can happen when using HTTP components * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. + * @see #RAW_TOKEN_PREFIX * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ public static Map<String, Object> parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException { - // must check for trailing & as the uri.split("&") will ignore those - if (!lenient) { - if (uri != null && uri.endsWith("&")) { - throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " - + "Check the uri and remove the trailing & marker."); - } - } - if (uri == null || ObjectHelper.isEmpty(uri)) { // return an empty map return new LinkedHashMap<>(0); } - // need to parse the uri query parameters manually as we cannot rely on splitting by &, - // as & can be used in a parameter value as well. - - try { - // use a linked map so the parameters is in the same order - Map<String, Object> rc = new LinkedHashMap<>(); - - boolean isKey = true; - boolean isValue = false; - boolean isRaw = false; - StringBuilder key = new StringBuilder(); - StringBuilder value = new StringBuilder(); - - // parse the uri parameters char by char - for (int i = 0; i < uri.length(); i++) { - // current char - char ch = uri.charAt(i); - // look ahead of the next char - char next; - if (i <= uri.length() - 2) { - next = uri.charAt(i + 1); - } else { - next = '\u0000'; - } - - // are we a raw value - isRaw = value.toString().startsWith(RAW_TOKEN_START); - - // if we are in raw mode, then we keep adding until we hit the end marker - if (isRaw) { - if (isKey) { - key.append(ch); - } else if (isValue) { - value.append(ch); - } - - // we only end the raw marker if its )& or at the end of the value - - boolean end = ch == RAW_TOKEN_END.charAt(0) && (next == '&' || next == '\u0000'); - if (end) { - // raw value end, so add that as a parameter, and reset flags - addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); - key.setLength(0); - value.setLength(0); - isKey = true; - isValue = false; - isRaw = false; - // skip to next as we are in raw mode and have already added the value - i++; - } - continue; - } - - // if its a key and there is a = sign then the key ends and we are in value mode - if (isKey && ch == '=') { - isKey = false; - isValue = true; - isRaw = false; - continue; - } - - // the & denote parameter is ended - if (ch == '&') { - // parameter is ended, as we hit & separator - addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); - key.setLength(0); - value.setLength(0); - isKey = true; - isValue = false; - isRaw = false; - continue; - } - - // regular char so add it to the key or value - if (isKey) { - key.append(ch); - } else if (isValue) { - value.append(ch); - } - } - - // any left over parameters, then add that - if (key.length() > 0) { - addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); - } + // must check for trailing & as the uri.split("&") will ignore those + if (!lenient && uri.endsWith("&")) { + throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + + "Check the uri and remove the trailing & marker."); + } - return rc; + URIScanner scanner = new URIScanner(CHARSET); + return scanner.parseQuery(uri, useRaw); + } - } catch (UnsupportedEncodingException e) { - URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); - se.initCause(e); - throw se; - } + /** + * Scans RAW tokens in the string and returns the list of pair indexes which tell where + * a RAW token starts and ends in the string. + * <p/> + * This is a companion method with {@link #isRaw(int, List)} and the returned value is + * supposed to be used as the parameter of that method. + * + * @param str the string to scan RAW tokens + * @return the list of pair indexes which represent the start and end positions of a RAW token + * @see #isRaw(int, List) + * @see #RAW_TOKEN_PREFIX + * @see #RAW_TOKEN_START + * @see #RAW_TOKEN_END + */ + public static List<Pair<Integer>> scanRaw(String str) { + return URIScanner.scanRaw(str); } - private static void addParameter(String name, String value, Map<String, Object> map, boolean isRaw) throws UnsupportedEncodingException { - name = URLDecoder.decode(name, CHARSET); - if (!isRaw) { - // need to replace % with %25 - String s = StringHelper.replaceAll(value, "%", "%25"); - value = URLDecoder.decode(s, CHARSET); - } - - // does the key already exist? - if (map.containsKey(name)) { - // yes it does, so make sure we can support multiple values, but using a list - // to hold the multiple values - Object existing = map.get(name); - List<String> list; - if (existing instanceof List) { - list = CastUtils.cast((List<?>) existing); - } else { - // create a new list to hold the multiple values - list = new ArrayList<>(); - String s = existing != null ? existing.toString() : null; - if (s != null) { - list.add(s); - } - } - list.add(value); - map.put(name, list); - } else { - map.put(name, value); - } + /** + * Tests if the index is within any pair of the start and end indexes which represent + * the start and end positions of a RAW token. + * <p/> + * This is a companion method with {@link #scanRaw(String)} and is supposed to consume + * the returned value of that method as the second parameter <tt>pairs</tt>. + * + * @param index the index to be tested + * @param pairs the list of pair indexes which represent the start and end positions of a RAW token + * @return <tt>true</tt> if the index is within any pair of the indexes, <tt>false</tt> otherwise + * @see #scanRaw(String) + * @see #RAW_TOKEN_PREFIX + * @see #RAW_TOKEN_START + * @see #RAW_TOKEN_END + */ + public static boolean isRaw(int index, List<Pair<Integer>> pairs) { + return URIScanner.isRaw(index, pairs); } /** @@ -335,35 +252,35 @@ public final class URISupport { * * @param parameters the uri parameters * @see #parseQuery(String) + * @see #RAW_TOKEN_PREFIX * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ @SuppressWarnings("unchecked") public static void resolveRawParameterValues(Map<String, Object> parameters) { for (Map.Entry<String, Object> entry : parameters.entrySet()) { - if (entry.getValue() != null) { - // if the value is a list then we need to iterate - Object value = entry.getValue(); - if (value instanceof List) { - List list = (List) value; - for (int i = 0; i < list.size(); i++) { - Object obj = list.get(i); - if (obj != null) { - String str = obj.toString(); - if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) { - str = str.substring(4, str.length() - 1); - // update the string in the list - list.set(i, str); - } - } - } - } else { - String str = entry.getValue().toString(); - if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) { - str = str.substring(4, str.length() - 1); - entry.setValue(str); + if (entry.getValue() == null) { + continue; + } + // if the value is a list then we need to iterate + Object value = entry.getValue(); + if (value instanceof List) { + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + Object obj = list.get(i); + if (obj == null) { + continue; } + String str = obj.toString(); + final int index = i; + URIScanner.resolveRaw(str, (s, raw) -> { + // update the string in the list + list.set(index, raw); + }); } + } else { + String str = entry.getValue().toString(); + URIScanner.resolveRaw(str, (s, raw) -> entry.setValue(raw)); } } } @@ -493,17 +410,19 @@ public final class URISupport { private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { rc.append(URLEncoder.encode(key, CHARSET)); + if (value == null) { + return; + } // only append if value is not null - if (value != null) { - rc.append("="); - if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) { - // do not encode RAW parameters unless it has % - // need to replace % with %25 to avoid losing "%" when decoding - String s = StringHelper.replaceAll(value, "%", "%25"); - rc.append(s); - } else { - rc.append(URLEncoder.encode(value, CHARSET)); - } + rc.append("="); + boolean isRaw = URIScanner.resolveRaw(value, (str, raw) -> { + // do not encode RAW parameters unless it has % + // need to replace % with %25 to avoid losing "%" when decoding + String s = StringHelper.replaceAll(str, "%", "%25"); + rc.append(s); + }); + if (!isRaw) { + rc.append(URLEncoder.encode(value, CHARSET)); } } @@ -551,6 +470,7 @@ public final class URISupport { * @return the normalized uri * @throws URISyntaxException in thrown if the uri syntax is invalid * @throws UnsupportedEncodingException is thrown if encoding error + * @see #RAW_TOKEN_PREFIX * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ diff --git a/camel-core/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java b/camel-core/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java index 8273c53..d98b88d 100644 --- a/camel-core/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java +++ b/camel-core/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java @@ -19,8 +19,6 @@ package org.apache.camel.util; import java.util.ArrayList; import java.util.BitSet; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Encoder for unsafe URI characters. @@ -32,7 +30,6 @@ public final class UnsafeUriCharactersEncoder { private static BitSet unsafeCharactersHttp; private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'}; - private static final Pattern RAW_PATTERN = Pattern.compile("RAW\\([^\\)]+\\)"); static { unsafeCharactersRfc1738 = new BitSet(256); @@ -94,48 +91,11 @@ public final class UnsafeUriCharactersEncoder { return encode(s, unsafeCharactersHttp, checkRaw); } - private static List<Pair> checkRAW(String s) { - Matcher matcher = RAW_PATTERN.matcher(s); - List<Pair> answer = new ArrayList<>(); - // Check all occurrences - while (matcher.find()) { - // TODO: should likely be matcher.end() - 1 - answer.add(new Pair(matcher.start(), matcher.end())); - } - return answer; - } - - private static boolean isRaw(int index, List<Pair>pairs) { - for (Pair pair : pairs) { - if (index < pair.left) { - return false; - } else { - if (index >= pair.left) { - if (index <= pair.right) { - return true; - } else { - continue; - } - } - } - } - return false; - } - - private static class Pair { - int left; - int right; - Pair(int left, int right) { - this.left = left; - this.right = right; - } - } - // Just skip the encode for isRAW part public static String encode(String s, BitSet unsafeCharacters, boolean checkRaw) { - List<Pair> rawPairs; + List<Pair<Integer>> rawPairs; if (checkRaw) { - rawPairs = checkRAW(s); + rawPairs = URISupport.scanRaw(s); } else { rawPairs = new ArrayList<>(); } @@ -170,7 +130,7 @@ public final class UnsafeUriCharactersEncoder { char next = i + 1 < chars.length ? chars[i + 1] : ' '; char next2 = i + 2 < chars.length ? chars[i + 2] : ' '; - if (isHexDigit(next) && isHexDigit(next2) && !isRaw(i, rawPairs)) { + if (isHexDigit(next) && isHexDigit(next2) && !URISupport.isRaw(i, rawPairs)) { // its already encoded (decimal encoded) so just append as is sb.append(ch); } else { diff --git a/camel-core/src/test/java/org/apache/camel/issues/EndpointWithRawUriParameterTest.java b/camel-core/src/test/java/org/apache/camel/issues/EndpointWithRawUriParameterTest.java index 7187274..2127a34 100644 --- a/camel-core/src/test/java/org/apache/camel/issues/EndpointWithRawUriParameterTest.java +++ b/camel-core/src/test/java/org/apache/camel/issues/EndpointWithRawUriParameterTest.java @@ -163,6 +163,17 @@ public class EndpointWithRawUriParameterTest extends ContextTestSupport { assertMockEndpointsSatisfied(); } + @Test + public void testRawUriParameterOkDynamic() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(1); + getMockEndpoint("mock:result").expectedHeaderReceived("username", "scott"); + getMockEndpoint("mock:result").expectedHeaderReceived("password", "foo)+bar"); + + template.sendBody("direct:okDynamic", "Hello World"); + + assertMockEndpointsSatisfied(); + } + @Override protected RouteBuilder createRouteBuilder() throws Exception { return new RouteBuilder() { @@ -189,6 +200,10 @@ public class EndpointWithRawUriParameterTest extends ContextTestSupport { from("direct:ok") .to("mycomponent:foo?password=RAW(foo)+bar)&username=scott") .to("mock:result"); + + from("direct:okDynamic") + .toD("mycomponent:foo?password=RAW{foo)+bar}&username=scott") + .to("mock:result"); } }; } diff --git a/camel-core/src/test/java/org/apache/camel/util/URISupportTest.java b/camel-core/src/test/java/org/apache/camel/util/URISupportTest.java index 11bf67f..63e5dc5 100644 --- a/camel-core/src/test/java/org/apache/camel/util/URISupportTest.java +++ b/camel-core/src/test/java/org/apache/camel/util/URISupportTest.java @@ -18,18 +18,23 @@ package org.apache.camel.util; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import org.apache.camel.ContextTestSupport; -import org.assertj.core.api.Assertions; import org.junit.Test; -/** - * @version - */ -public class URISupportTest extends ContextTestSupport { +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class URISupportTest { @Test public void testNormalizeEndpointUri() throws Exception { @@ -256,16 +261,20 @@ public class URISupportTest extends ContextTestSupport { @Test public void testSanitizeUriWithRawPassword() { - String uri = "http://foo?username=me&password=RAW(me#@123)&foo=bar"; + String uri1 = "http://foo?username=me&password=RAW(me#@123)&foo=bar"; + String uri2 = "http://foo?username=me&password=RAW{me#@123}&foo=bar"; String expected = "http://foo?username=me&password=xxxxxx&foo=bar"; - assertEquals(expected, URISupport.sanitizeUri(uri)); + assertEquals(expected, URISupport.sanitizeUri(uri1)); + assertEquals(expected, URISupport.sanitizeUri(uri2)); } @Test public void testSanitizeUriRawUnsafePassword() { - String uri = "sftp://localhost/target?password=RAW(beforeAmp&afterAmp)&username=jrandom"; + String uri1 = "sftp://localhost/target?password=RAW(beforeAmp&afterAmp)&username=jrandom"; + String uri2 = "sftp://localhost/target?password=RAW{beforeAmp&afterAmp}&username=jrandom"; String expected = "sftp://localhost/target?password=xxxxxx&username=jrandom"; - assertEquals(expected, URISupport.sanitizeUri(uri)); + assertEquals(expected, URISupport.sanitizeUri(uri1)); + assertEquals(expected, URISupport.sanitizeUri(uri2)); } @Test @@ -300,6 +309,16 @@ public class URISupportTest extends ContextTestSupport { } @Test + public void testRawParameterCurly() throws Exception { + String out = URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{++?w0rd}&serviceName=some chat"); + assertEquals("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{++?w0rd}&serviceName=some+chat", out); + + String out2 = URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{foo %% bar}&serviceName=some chat"); + // Just make sure the RAW parameter can be resolved rightly, we need to replace the % into %25 + assertEquals("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{foo %25%25 bar}&serviceName=some+chat", out2); + } + + @Test public void testParseQuery() throws Exception { Map<String, Object> map = URISupport.parseQuery("password=secret&serviceName=somechat"); assertEquals(2, map.size()); @@ -323,6 +342,24 @@ public class URISupportTest extends ContextTestSupport { } @Test + public void testParseQueryCurly() throws Exception { + Map<String, Object> map = URISupport.parseQuery("password=RAW{++?w0rd}&serviceName=somechat"); + assertEquals(2, map.size()); + assertEquals("RAW{++?w0rd}", map.get("password")); + assertEquals("somechat", map.get("serviceName")); + + map = URISupport.parseQuery("password=RAW{++?)w&rd}&serviceName=somechat"); + assertEquals(2, map.size()); + assertEquals("RAW{++?)w&rd}", map.get("password")); + assertEquals("somechat", map.get("serviceName")); + + map = URISupport.parseQuery("password=RAW{%2520w&rd}&serviceName=somechat"); + assertEquals(2, map.size()); + assertEquals("RAW{%2520w&rd}", map.get("password")); + assertEquals("somechat", map.get("serviceName")); + } + + @Test public void testParseQueryLenient() throws Exception { try { URISupport.parseQuery("password=secret&serviceName=somechat&", false, false); @@ -338,6 +375,48 @@ public class URISupportTest extends ContextTestSupport { } @Test + public void testScanRaw() { + List<Pair<Integer>> pairs1 = URISupport.scanRaw("password=RAW(++?5w0rd)&serviceName=somechat"); + assertEquals(1, pairs1.size()); + assertEquals(new Pair(9, 21), pairs1.get(0)); + + List<Pair<Integer>> pairs2 = URISupport.scanRaw("password=RAW{++?5w0rd}&serviceName=somechat"); + assertEquals(1, pairs2.size()); + assertEquals(new Pair(9, 21), pairs2.get(0)); + + List<Pair<Integer>> pairs3 = URISupport.scanRaw("password=RAW{++?)&0rd}&serviceName=somechat"); + assertEquals(1, pairs3.size()); + assertEquals(new Pair(9, 21), pairs3.get(0)); + + List<Pair<Integer>> pairs4 = URISupport.scanRaw("password1=RAW(++?}&0rd)&password2=RAW{++?)&0rd}&serviceName=somechat"); + assertEquals(2, pairs4.size()); + assertEquals(new Pair(10, 22), pairs4.get(0)); + assertEquals(new Pair(34, 46), pairs4.get(1)); + } + + @Test + public void testIsRaw() { + List<Pair<Integer>> pairs = Arrays.asList( + new Pair(3, 5), + new Pair(8, 10)); + for (int i = 0; i < 3; i++) { + assertFalse(URISupport.isRaw(i, pairs)); + } + for (int i = 3; i < 6; i++) { + assertTrue(URISupport.isRaw(i, pairs)); + } + for (int i = 6; i < 8; i++) { + assertFalse(URISupport.isRaw(i, pairs)); + } + for (int i = 8; i < 11; i++) { + assertTrue(URISupport.isRaw(i, pairs)); + } + for (int i = 11; i < 15; i++) { + assertFalse(URISupport.isRaw(i, pairs)); + } + } + + @Test public void testResolveRawParameterValues() throws Exception { Map<String, Object> map = URISupport.parseQuery("password=secret&serviceName=somechat"); URISupport.resolveRawParameterValues(map); @@ -359,6 +438,21 @@ public class URISupportTest extends ContextTestSupport { } @Test + public void testResolveRawParameterValuesCurly() throws Exception { + Map<String, Object> map = URISupport.parseQuery("password=RAW{++?w0rd}&serviceName=somechat"); + URISupport.resolveRawParameterValues(map); + assertEquals(2, map.size()); + assertEquals("++?w0rd", map.get("password")); + assertEquals("somechat", map.get("serviceName")); + + map = URISupport.parseQuery("password=RAW{++?)w&rd}&serviceName=somechat"); + URISupport.resolveRawParameterValues(map); + assertEquals(2, map.size()); + assertEquals("++?)w&rd", map.get("password")); + assertEquals("somechat", map.get("serviceName")); + } + + @Test public void testAppendParameterToUriAndReplaceExistingOne() throws Exception { Map<String, Object> newParameters = new HashMap<>(); newParameters.put("foo", "456"); @@ -380,45 +474,45 @@ public class URISupportTest extends ContextTestSupport { @Test public void shouldStripPrefixes() { - Assertions.assertThat(URISupport.stripPrefix(null, null)).isNull(); - Assertions.assertThat(URISupport.stripPrefix("", null)).isEmpty(); - Assertions.assertThat(URISupport.stripPrefix(null, "")).isNull(); - Assertions.assertThat(URISupport.stripPrefix("", "")).isEmpty(); - Assertions.assertThat(URISupport.stripPrefix("a", "b")).isEqualTo("a"); - Assertions.assertThat(URISupport.stripPrefix("a", "a")).isEmpty(); - Assertions.assertThat(URISupport.stripPrefix("ab", "b")).isEqualTo("ab"); - Assertions.assertThat(URISupport.stripPrefix("a", "ab")).isEqualTo("a"); + assertThat(URISupport.stripPrefix(null, null)).isNull(); + assertThat(URISupport.stripPrefix("", null)).isEmpty(); + assertThat(URISupport.stripPrefix(null, "")).isNull(); + assertThat(URISupport.stripPrefix("", "")).isEmpty(); + assertThat(URISupport.stripPrefix("a", "b")).isEqualTo("a"); + assertThat(URISupport.stripPrefix("a", "a")).isEmpty(); + assertThat(URISupport.stripPrefix("ab", "b")).isEqualTo("ab"); + assertThat(URISupport.stripPrefix("a", "ab")).isEqualTo("a"); } @Test public void shouldStripSuffixes() { - Assertions.assertThat(URISupport.stripSuffix(null, null)).isNull(); - Assertions.assertThat(URISupport.stripSuffix("", null)).isEmpty(); - Assertions.assertThat(URISupport.stripSuffix(null, "")).isNull(); - Assertions.assertThat(URISupport.stripSuffix("", "")).isEmpty(); - Assertions.assertThat(URISupport.stripSuffix("a", "b")).isEqualTo("a"); - Assertions.assertThat(URISupport.stripSuffix("a", "a")).isEmpty(); - Assertions.assertThat(URISupport.stripSuffix("ab", "b")).isEqualTo("a"); - Assertions.assertThat(URISupport.stripSuffix("a", "ab")).isEqualTo("a"); + assertThat(URISupport.stripSuffix(null, null)).isNull(); + assertThat(URISupport.stripSuffix("", null)).isEmpty(); + assertThat(URISupport.stripSuffix(null, "")).isNull(); + assertThat(URISupport.stripSuffix("", "")).isEmpty(); + assertThat(URISupport.stripSuffix("a", "b")).isEqualTo("a"); + assertThat(URISupport.stripSuffix("a", "a")).isEmpty(); + assertThat(URISupport.stripSuffix("ab", "b")).isEqualTo("a"); + assertThat(URISupport.stripSuffix("a", "ab")).isEqualTo("a"); } @Test public void shouldJoinPaths() { - Assertions.assertThat(URISupport.joinPaths(null, null)).isEmpty(); - Assertions.assertThat(URISupport.joinPaths("", null)).isEmpty(); - Assertions.assertThat(URISupport.joinPaths(null, "")).isEmpty(); - Assertions.assertThat(URISupport.joinPaths("", "")).isEmpty(); - Assertions.assertThat(URISupport.joinPaths("a", "")).isEqualTo("a"); - Assertions.assertThat(URISupport.joinPaths("a", "b")).isEqualTo("a/b"); - Assertions.assertThat(URISupport.joinPaths("/a", "b")).isEqualTo("/a/b"); - Assertions.assertThat(URISupport.joinPaths("/a", "b/")).isEqualTo("/a/b/"); - Assertions.assertThat(URISupport.joinPaths("/a/", "b/")).isEqualTo("/a/b/"); - Assertions.assertThat(URISupport.joinPaths("/a/", "/b/")).isEqualTo("/a/b/"); - Assertions.assertThat(URISupport.joinPaths("a", "b", "c")).isEqualTo("a/b/c"); - Assertions.assertThat(URISupport.joinPaths("a", null, "c")).isEqualTo("a/c"); - Assertions.assertThat(URISupport.joinPaths("/a/", "/b", "c/", "/d/")).isEqualTo("/a/b/c/d/"); - Assertions.assertThat(URISupport.joinPaths("/a/", "/b", "c/", null)).isEqualTo("/a/b/c/"); - Assertions.assertThat(URISupport.joinPaths("/a/", null, null, null)).isEqualTo("/a/"); - Assertions.assertThat(URISupport.joinPaths("a/", "/b", null, null)).isEqualTo("a/b"); - } -} \ No newline at end of file + assertThat(URISupport.joinPaths(null, null)).isEmpty(); + assertThat(URISupport.joinPaths("", null)).isEmpty(); + assertThat(URISupport.joinPaths(null, "")).isEmpty(); + assertThat(URISupport.joinPaths("", "")).isEmpty(); + assertThat(URISupport.joinPaths("a", "")).isEqualTo("a"); + assertThat(URISupport.joinPaths("a", "b")).isEqualTo("a/b"); + assertThat(URISupport.joinPaths("/a", "b")).isEqualTo("/a/b"); + assertThat(URISupport.joinPaths("/a", "b/")).isEqualTo("/a/b/"); + assertThat(URISupport.joinPaths("/a/", "b/")).isEqualTo("/a/b/"); + assertThat(URISupport.joinPaths("/a/", "/b/")).isEqualTo("/a/b/"); + assertThat(URISupport.joinPaths("a", "b", "c")).isEqualTo("a/b/c"); + assertThat(URISupport.joinPaths("a", null, "c")).isEqualTo("a/c"); + assertThat(URISupport.joinPaths("/a/", "/b", "c/", "/d/")).isEqualTo("/a/b/c/d/"); + assertThat(URISupport.joinPaths("/a/", "/b", "c/", null)).isEqualTo("/a/b/c/"); + assertThat(URISupport.joinPaths("/a/", null, null, null)).isEqualTo("/a/"); + assertThat(URISupport.joinPaths("a/", "/b", null, null)).isEqualTo("a/b"); + } +} diff --git a/platforms/camel-catalog/pom.xml b/platforms/camel-catalog/pom.xml index 9567c5e..ba19bbe 100644 --- a/platforms/camel-catalog/pom.xml +++ b/platforms/camel-catalog/pom.xml @@ -132,6 +132,7 @@ <include>JSonSchemaHelper.java</include> <include>JSonSchemaResolver.java</include> <include>LanguageValidationResult.java</include> + <include>Pair.java</include> <include>SimpleValidationResult.java</include> <include>SuggestionStrategy.java</include> <include>TimePatternConverter.java</include> @@ -166,6 +167,7 @@ <include>${basedir}/src/main/java/org/apache/camel/catalog/JSonSchemaHelper.java</include> <include>${basedir}/src/main/java/org/apache/camel/catalog/JSonSchemaResolver.java</include> <include>${basedir}/src/main/java/org/apache/camel/catalog/LanguageValidationResult.java</include> + <include>${basedir}/src/main/java/org/apache/camel/catalog/Pair.java</include> <include>${basedir}/src/main/java/org/apache/camel/catalog/SimpleValidationResult.java</include> <include>${basedir}/src/main/java/org/apache/camel/catalog/SuggestionStrategy.java</include> <include>${basedir}/src/main/java/org/apache/camel/catalog/TimePatternConverter.java</include> diff --git a/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/Pair.java b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/Pair.java new file mode 100644 index 0000000..349d3d0 --- /dev/null +++ b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/Pair.java @@ -0,0 +1,60 @@ +/** + * 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.catalog; + +import java.util.Objects; + +/** + * Copied from org.apache.camel.util.Pair + */ +public class Pair<T> { + + private T left; + private T right; + + public Pair(T left, T right) { + this.left = left; + this.right = right; + } + + public T getLeft() { + return left; + } + + public T getRight() { + return right; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair<?> that = (Pair<?>) o; + return Objects.equals(left, that.left) && + Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } + + @Override + public String toString() { + return "(" + left + ", " + right + ")"; + } +} diff --git a/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/URISupport.java b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/URISupport.java index f2079c1..12532e6 100644 --- a/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/URISupport.java +++ b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/URISupport.java @@ -26,14 +26,18 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; + +import org.apache.camel.catalog.Pair; /** * Copied from org.apache.camel.util.URISupport */ public final class URISupport { - public static final String RAW_TOKEN_START = "RAW("; - public static final String RAW_TOKEN_END = ")"; + public static final String RAW_TOKEN_PREFIX = "RAW"; + public static final char[] RAW_TOKEN_START = { '(', '{' }; + public static final char[] RAW_TOKEN_END = { ')', '}' }; private static final String CHARSET = "UTF-8"; @@ -155,17 +159,17 @@ public final class URISupport { * @see #RAW_TOKEN_END */ public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { - // must check for trailing & as the uri.split("&") will ignore those - if (uri != null && uri.endsWith("&")) { - throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " - + "Check the uri and remove the trailing & marker."); - } - if (isEmpty(uri)) { // return an empty map return new LinkedHashMap<>(0); } + // must check for trailing & as the uri.split("&") will ignore those + if (uri.endsWith("&")) { + throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + + "Check the uri and remove the trailing & marker."); + } + // need to parse the uri query parameters manually as we cannot rely on splitting by &, // as & can be used in a parameter value as well. @@ -192,7 +196,15 @@ public final class URISupport { } // are we a raw value - isRaw = value.toString().startsWith(RAW_TOKEN_START); + char rawTokenEnd = 0; + for (int j = 0; j < RAW_TOKEN_START.length; j++) { + String rawTokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[j]; + isRaw = value.toString().startsWith(rawTokenStart); + if (isRaw) { + rawTokenEnd = RAW_TOKEN_END[j]; + break; + } + } // if we are in raw mode, then we keep adding until we hit the end marker if (isRaw) { @@ -202,9 +214,9 @@ public final class URISupport { value.append(ch); } - // we only end the raw marker if its )& or at the end of the value + // we only end the raw marker if it's ")&", "}&", or at the end of the value - boolean end = ch == RAW_TOKEN_END.charAt(0) && (next == '&' || next == '\u0000'); + boolean end = ch == rawTokenEnd && (next == '&' || next == '\u0000'); if (end) { // raw value end, so add that as a parameter, and reset flags addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); @@ -302,6 +314,71 @@ public final class URISupport { } } + public static List<Pair<Integer>> scanRaw(String str) { + List<Pair<Integer>> answer = new ArrayList<>(); + if (str == null || isEmpty(str)) { + return answer; + } + + int offset = 0; + int start = str.indexOf(RAW_TOKEN_PREFIX); + while (start >= 0 && offset < str.length()) { + offset = start + RAW_TOKEN_PREFIX.length(); + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + char tokenEnd = RAW_TOKEN_END[i]; + if (str.startsWith(tokenStart, start)) { + offset = scanRawToEnd(str, start, tokenStart, tokenEnd, answer); + continue; + } + } + start = str.indexOf(RAW_TOKEN_PREFIX, offset); + } + return answer; + } + + private static int scanRawToEnd(String str, int start, String tokenStart, char tokenEnd, + List<Pair<Integer>> answer) { + // we search the first end bracket to close the RAW token + // as opposed to parsing query, this doesn't allow the occurrences of end brackets + // inbetween because this may be used on the host/path parts of URI + // and thus we cannot rely on '&' for detecting the end of a RAW token + int end = str.indexOf(tokenEnd, start + tokenStart.length()); + if (end < 0) { + // still return a pair even if RAW token is not closed + answer.add(new Pair<>(start, str.length())); + return str.length(); + } + answer.add(new Pair<>(start, end)); + return end + 1; + } + + public static boolean isRaw(int index, List<Pair<Integer>> pairs) { + for (Pair<Integer> pair : pairs) { + if (index < pair.getLeft()) { + return false; + } + if (index <= pair.getRight()) { + return true; + } + } + return false; + } + + private static boolean resolveRaw(String str, BiConsumer<String, String> consumer) { + for (int i = 0; i < RAW_TOKEN_START.length; i++) { + String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; + String tokenEnd = String.valueOf(RAW_TOKEN_END[i]); + if (str.startsWith(tokenStart) && str.endsWith(tokenEnd)) { + String raw = str.substring(tokenStart.length(), str.length() - 1); + consumer.accept(str, raw); + return true; + } + } + // not RAW value + return false; + } + /** * Assembles a query from the given map. * @@ -346,18 +423,20 @@ public final class URISupport { } else { rc.append(key); } + if (value == null) { + return; + } // only append if value is not null - if (value != null) { - rc.append("="); - if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) { - // do not encode RAW parameters - rc.append(value); + rc.append("="); + boolean isRaw = resolveRaw(value, (str, raw) -> { + // do not encode RAW parameters + rc.append(str); + }); + if (!isRaw) { + if (encode) { + rc.append(URLEncoder.encode(value, CHARSET)); } else { - if (encode) { - rc.append(URLEncoder.encode(value, CHARSET)); - } else { - rc.append(value); - } + rc.append(value); } } } diff --git a/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/UnsafeUriCharactersEncoder.java b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/UnsafeUriCharactersEncoder.java index 04d7753..07c76bb 100644 --- a/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/UnsafeUriCharactersEncoder.java +++ b/platforms/camel-catalog/src/main/java/org/apache/camel/catalog/UnsafeUriCharactersEncoder.java @@ -19,8 +19,8 @@ package org.apache.camel.catalog; import java.util.ArrayList; import java.util.BitSet; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import org.apache.camel.catalog.Pair; /** * Encoder for unsafe URI characters. @@ -32,7 +32,6 @@ public final class UnsafeUriCharactersEncoder { private static BitSet unsafeCharactersHttp; private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'}; - private static final Pattern RAW_PATTERN = Pattern.compile("RAW\\([^\\)]+\\)"); static { unsafeCharactersRfc1738 = new BitSet(256); @@ -94,48 +93,11 @@ public final class UnsafeUriCharactersEncoder { return encode(s, unsafeCharactersHttp, checkRaw); } - private static List<Pair> checkRAW(String s) { - Matcher matcher = RAW_PATTERN.matcher(s); - List<Pair> answer = new ArrayList<>(); - // Check all occurrences - while (matcher.find()) { - answer.add(new Pair(matcher.start(), matcher.end())); - } - return answer; - } - - private static boolean isRaw(int index, List<Pair> pairs) { - for (Pair pair : pairs) { - if (index < pair.left) { - return false; - } else { - if (index >= pair.left) { - if (index <= pair.right) { - return true; - } else { - continue; - } - } - } - } - return false; - } - - private static class Pair { - int left; - int right; - - Pair(int left, int right) { - this.left = left; - this.right = right; - } - } - // Just skip the encode for isRAW part public static String encode(String s, BitSet unsafeCharacters, boolean checkRaw) { - List<Pair> rawPairs; + List<Pair<Integer>> rawPairs; if (checkRaw) { - rawPairs = checkRAW(s); + rawPairs = URISupport.scanRaw(s); } else { rawPairs = new ArrayList<>(); } @@ -170,7 +132,7 @@ public final class UnsafeUriCharactersEncoder { char next = i + 1 < chars.length ? chars[i + 1] : ' '; char next2 = i + 2 < chars.length ? chars[i + 2] : ' '; - if (isHexDigit(next) && isHexDigit(next2) && !isRaw(i, rawPairs)) { + if (isHexDigit(next) && isHexDigit(next2) && !URISupport.isRaw(i, rawPairs)) { // its already encoded (decimal encoded) so just append as is sb.append(ch); } else {