Skip to content

Commit

Permalink
[http] Properly escape + character in query string (openhab#17042)
Browse files Browse the repository at this point in the history
* [http] Properly escape + character in query string

Signed-off-by: Jan N. Klug <[email protected]>
  • Loading branch information
J-N-K authored and andrewfg committed Jul 18, 2024
1 parent f04c635 commit bea34d9
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ private void sendHttpValue(String commandUrl, String command) {
private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
try {
// format URL
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command));
URI uri = Util.uriFromString(Util.wrappedStringFormat(commandUrl, new Date(), command));

// build request
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

Expand All @@ -34,8 +36,10 @@
@NonNullByDefault
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
* @return the string representing the request
Expand All @@ -51,17 +55,33 @@ public static String requestToLogString(Request request) {
}

/**
* 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
* @return URI corresponding to the input string
* @throws MalformedURLException if parameter is not an URL
* @throws URISyntaxException if parameter could not be converted to an URI
* @throws MalformedURLException if parameter is not a URL
* @throws URISyntaxException if parameter could not be converted to a URI
*/
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
URL url = new URL(s);
URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(),
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private void refresh(boolean isRetry) {

// format URL
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);

httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
*/
package org.openhab.binding.http;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Date;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openhab.binding.http.internal.Util;

Expand All @@ -31,30 +34,61 @@ public class UtilTest {
@Test
public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException {
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
public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException {
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
public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException {
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
public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException {
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
public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
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));
}
}

0 comments on commit bea34d9

Please sign in to comment.