[http] Properly escape + character in query string (#17042)

* [http] Properly escape + character in query string

Signed-off-by: Jan N. Klug <github@klug.nrw>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
J-N-K 2024-07-12 22:02:44 +02:00 committed by Ciprian Pascu
parent b87e22bc0c
commit e5510f66bf
4 changed files with 67 additions and 13 deletions

View File

@ -369,7 +369,7 @@ public class HttpThingHandler extends BaseThingHandler implements HttpStatusList
private void sendHttpValue(String commandUrl, String command, boolean isRetry) { private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
try { try {
// format URL // format URL
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command)); URI uri = Util.uriFromString(Util.wrappedStringFormat(commandUrl, new Date(), command));
// build request // build request
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType) rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)

View File

@ -18,6 +18,8 @@ import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@ -34,8 +36,10 @@ import org.eclipse.jetty.http.HttpField;
@NonNullByDefault @NonNullByDefault
public class Util { public class Util {
public static final Pattern FORMAT_REPLACE_PATTERN = Pattern.compile("%\\d\\$[^%]+");
/** /**
* create a log string from a {@link org.eclipse.jetty.client.api.Request} * Create a log string from a {@link org.eclipse.jetty.client.api.Request}
* *
* @param request the request to log * @param request the request to log
* @return the string representing the request * @return the string representing the request
@ -51,17 +55,33 @@ public class Util {
} }
/** /**
* create an URI from a string, escaping all necessary characters * Create a URI from a string, escaping all necessary characters
* *
* @param s the URI as unescaped string * @param s the URI as unescaped string
* @return URI corresponding to the input string * @return URI corresponding to the input string
* @throws MalformedURLException if parameter is not an URL * @throws MalformedURLException if parameter is not a URL
* @throws URISyntaxException if parameter could not be converted to an URI * @throws URISyntaxException if parameter could not be converted to a URI
*/ */
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException { public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
URL url = new URL(s); URL url = new URL(s);
URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(),
url.getPath(), url.getQuery(), url.getRef()); url.getPath(), url.getQuery(), url.getRef());
return URI.create(uri.toASCIIString()); return URI.create(uri.toASCIIString().replace("+", "%2B"));
}
/**
* Format a string using {@link String#format(String, Object...)} but allow non-format percent characters
*
* The {@param inputString} is checked for format patterns ({@code %<index>$<format>}) and passes only those to the
* {@link String#format(String, Object...)} method. This avoids format errors due to other percent characters in the
* string.
*
* @param inputString the input string, potentially containing format instructions
* @param params an array of parameters to be passed to the splitted input string
* @return the formatted string
*/
public static String wrappedStringFormat(String inputString, Object... params) {
Matcher replaceMatcher = FORMAT_REPLACE_PATTERN.matcher(inputString);
return replaceMatcher.replaceAll(matchResult -> String.format(matchResult.group(), params));
} }
} }

View File

@ -108,7 +108,7 @@ public class RefreshingUrlCache {
// format URL // format URL
try { try {
URI uri = Util.uriFromString(String.format(this.url, new Date())); URI uri = Util.uriFromString(Util.wrappedStringFormat(this.url, new Date()));
logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout); logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> { httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.http; package org.openhab.binding.http;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.binding.http.internal.Util; import org.openhab.binding.http.internal.Util;
@ -31,30 +34,61 @@ public class UtilTest {
@Test @Test
public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException { public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foöo.bar/zhu.html?str=zin&tzz=678"; String s = "https://foöo.bar/zhu.html?str=zin&tzz=678";
Assertions.assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString()); assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString());
} }
@Test @Test
public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException { public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/zül.html?str=zin"; String s = "https://foo.bar/zül.html?str=zin";
Assertions.assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString()); assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString());
} }
@Test @Test
public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException { public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/zil.html?str=zän"; String s = "https://foo.bar/zil.html?str=zän";
Assertions.assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString()); assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString());
} }
@Test @Test
public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException { public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/z l.html?str=zun"; String s = "https://foo.bar/z l.html?str=zun";
Assertions.assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString()); assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString());
} }
@Test @Test
public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException { public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/zzl.html?str=z n"; String s = "https://foo.bar/zzl.html?str=z n";
Assertions.assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString()); assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString());
}
@Test
public void uriPlusInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/zzl.html?str=z+n";
assertEquals("https://foo.bar/zzl.html?str=z%2Bn", Util.uriFromString(s).toString());
}
@Test
public void uriAlreadyPartlyEscapedTest() throws MalformedURLException, URISyntaxException {
String s = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01";
assertEquals(
"https://foo.bar/zzl.html?p=field%252Bvalue&foostatus=This%20is%20a%20test%20String&date=2024-%20%2007-01",
Util.uriFromString(s).toString());
}
@Test
public void wrappedStringFormatDateTest() {
String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&date=%1$tY-%1$4tm-%1$td";
Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z"));
assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&date=2024- 07-01",
Util.wrappedStringFormat(formatString, testDate));
}
@Test
public void wrappedStringFormatDateAndCommandTest() {
String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=%2$s&date=%1$tY-%1$4tm-%1$td";
Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z"));
String testCommand = "This is a test String";
assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01",
Util.wrappedStringFormat(formatString, testDate, testCommand));
} }
} }