Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}