Skip to content

Commit f8e5644

Browse files
authored
[http] Properly escape + character in query string (openhab#17042)
* [http] Properly escape + character in query string Signed-off-by: Jan N. Klug <github@klug.nrw>
1 parent 790cc88 commit f8e5644

File tree

4 files changed

+67
-13
lines changed

4 files changed

+67
-13
lines changed

bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ private void sendHttpValue(String commandUrl, String command) {
369369
private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
370370
try {
371371
// format URL
372-
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command));
372+
URI uri = Util.uriFromString(Util.wrappedStringFormat(commandUrl, new Date(), command));
373373

374374
// build request
375375
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)

bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java

+25-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.net.URISyntaxException;
1919
import java.net.URL;
2020
import java.nio.charset.StandardCharsets;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
2123
import java.util.stream.Collectors;
2224
import java.util.stream.StreamSupport;
2325

@@ -34,8 +36,10 @@
3436
@NonNullByDefault
3537
public class Util {
3638

39+
public static final Pattern FORMAT_REPLACE_PATTERN = Pattern.compile("%\\d\\$[^%]+");
40+
3741
/**
38-
* create a log string from a {@link org.eclipse.jetty.client.api.Request}
42+
* Create a log string from a {@link org.eclipse.jetty.client.api.Request}
3943
*
4044
* @param request the request to log
4145
* @return the string representing the request
@@ -51,17 +55,33 @@ public static String requestToLogString(Request request) {
5155
}
5256

5357
/**
54-
* create an URI from a string, escaping all necessary characters
58+
* Create a URI from a string, escaping all necessary characters
5559
*
5660
* @param s the URI as unescaped string
5761
* @return URI corresponding to the input string
58-
* @throws MalformedURLException if parameter is not an URL
59-
* @throws URISyntaxException if parameter could not be converted to an URI
62+
* @throws MalformedURLException if parameter is not a URL
63+
* @throws URISyntaxException if parameter could not be converted to a URI
6064
*/
6165
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
6266
URL url = new URL(s);
6367
URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(),
6468
url.getPath(), url.getQuery(), url.getRef());
65-
return URI.create(uri.toASCIIString());
69+
return URI.create(uri.toASCIIString().replace("+", "%2B"));
70+
}
71+
72+
/**
73+
* Format a string using {@link String#format(String, Object...)} but allow non-format percent characters
74+
*
75+
* The {@param inputString} is checked for format patterns ({@code %<index>$<format>}) and passes only those to the
76+
* {@link String#format(String, Object...)} method. This avoids format errors due to other percent characters in the
77+
* string.
78+
*
79+
* @param inputString the input string, potentially containing format instructions
80+
* @param params an array of parameters to be passed to the splitted input string
81+
* @return the formatted string
82+
*/
83+
public static String wrappedStringFormat(String inputString, Object... params) {
84+
Matcher replaceMatcher = FORMAT_REPLACE_PATTERN.matcher(inputString);
85+
return replaceMatcher.replaceAll(matchResult -> String.format(matchResult.group(), params));
6686
}
6787
}

bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private void refresh(boolean isRetry) {
108108

109109
// format URL
110110
try {
111-
URI uri = Util.uriFromString(String.format(this.url, new Date()));
111+
URI uri = Util.uriFromString(Util.wrappedStringFormat(this.url, new Date()));
112112
logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
113113

114114
httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {

bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/UtilTest.java

+40-6
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
*/
1313
package org.openhab.binding.http;
1414

15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
1517
import java.net.MalformedURLException;
1618
import java.net.URISyntaxException;
19+
import java.time.Instant;
20+
import java.util.Date;
1721

1822
import org.eclipse.jdt.annotation.NonNullByDefault;
19-
import org.junit.jupiter.api.Assertions;
2023
import org.junit.jupiter.api.Test;
2124
import org.openhab.binding.http.internal.Util;
2225

@@ -31,30 +34,61 @@ public class UtilTest {
3134
@Test
3235
public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException {
3336
String s = "https://foöo.bar/zhu.html?str=zin&tzz=678";
34-
Assertions.assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString());
37+
assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString());
3538
}
3639

3740
@Test
3841
public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException {
3942
String s = "https://foo.bar/zül.html?str=zin";
40-
Assertions.assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString());
43+
assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString());
4144
}
4245

4346
@Test
4447
public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException {
4548
String s = "https://foo.bar/zil.html?str=zän";
46-
Assertions.assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString());
49+
assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString());
4750
}
4851

4952
@Test
5053
public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException {
5154
String s = "https://foo.bar/z l.html?str=zun";
52-
Assertions.assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString());
55+
assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString());
5356
}
5457

5558
@Test
5659
public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
5760
String s = "https://foo.bar/zzl.html?str=z n";
58-
Assertions.assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString());
61+
assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString());
62+
}
63+
64+
@Test
65+
public void uriPlusInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
66+
String s = "https://foo.bar/zzl.html?str=z+n";
67+
assertEquals("https://foo.bar/zzl.html?str=z%2Bn", Util.uriFromString(s).toString());
68+
}
69+
70+
@Test
71+
public void uriAlreadyPartlyEscapedTest() throws MalformedURLException, URISyntaxException {
72+
String s = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01";
73+
assertEquals(
74+
"https://foo.bar/zzl.html?p=field%252Bvalue&foostatus=This%20is%20a%20test%20String&date=2024-%20%2007-01",
75+
Util.uriFromString(s).toString());
76+
}
77+
78+
@Test
79+
public void wrappedStringFormatDateTest() {
80+
String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&date=%1$tY-%1$4tm-%1$td";
81+
Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z"));
82+
assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&date=2024- 07-01",
83+
Util.wrappedStringFormat(formatString, testDate));
84+
}
85+
86+
@Test
87+
public void wrappedStringFormatDateAndCommandTest() {
88+
String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=%2$s&date=%1$tY-%1$4tm-%1$td";
89+
Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z"));
90+
String testCommand = "This is a test String";
91+
assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01",
92+
Util.wrappedStringFormat(formatString, testDate, testCommand));
5993
}
6094
}

0 commit comments

Comments
 (0)