Skip to content

Commit

Permalink
fix(rest): Ensure proper URL encoding + fix case issues with content …
Browse files Browse the repository at this point in the history
…type headers (#2826)

* fix(rest): Ensure proper URL encoding + fix case issues with content type headers

* fix(rest): Add error logs

* fix(rest): Refactor code structure

* fix(rest): Make the encoder static

* fix(rest): Fix license
  • Loading branch information
johnBgood authored Jul 8, 2024
1 parent 75d79ab commit e07b88c
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ public void build(ClassicRequestBuilder builder, HttpCommonRequest request) {
private HttpEntity createEntityForContentType(
ContentType contentType, Map<?, ?> body, HttpCommonRequest request) {
HttpEntity entity;
if (contentType.getMimeType().equals(MULTIPART_FORM_DATA.getMimeType())) {
if (contentType.getMimeType().equalsIgnoreCase(MULTIPART_FORM_DATA.getMimeType())) {
entity = createMultiPartEntity(body, contentType);
} else if (contentType
.getMimeType()
.equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
.equalsIgnoreCase(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
entity = createUrlEncodedFormEntity(body);
} else {
entity = createStringEntity(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,12 @@
package io.camunda.connector.http.base.client.apache.builder.parts;

import io.camunda.connector.http.base.model.HttpCommonRequest;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;

public class ApacheRequestUriBuilder implements ApacheRequestPartBuilder {

@Override
public void build(ClassicRequestBuilder builder, HttpCommonRequest request) {
try {
var url = new URL(request.getUrl());
builder.setUri(
// Only this URI constructor escapes the URL properly
new URI(
url.getProtocol(),
url.getUserInfo(),
url.getHost(),
url.getPort(),
url.getPath(),
url.getQuery(),
null));
} catch (MalformedURLException | URISyntaxException e) {
builder.setUri(request.getUrl());
}
builder.setUri(UrlEncoder.toEncodedUri(request.getUrl()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; 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 io.camunda.connector.http.base.client.apache.builder.parts;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UrlEncoder {
private static final Logger LOG = LoggerFactory.getLogger(ApacheRequestUriBuilder.class);

public static URI toEncodedUri(String requestUrl) {
try {
// We try to decode the URL first, because it might be encoded already
// which would lead to double encoding. Decoding is safe here, because it does nothing if
// the URL is not encoded.
var decodedUrl = URLDecoder.decode(requestUrl, StandardCharsets.UTF_8);
var url = new URL(decodedUrl);
// Only this URI constructor escapes the URL properly
return new URI(
url.getProtocol(),
url.getUserInfo(),
url.getHost(),
url.getPort(),
url.getPath(),
url.getQuery(),
null);
} catch (MalformedURLException | URISyntaxException e) {
LOG.error("Failed to parse URL {}, defaulting to requestUrl", requestUrl, e);
return URI.create(requestUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@
import io.camunda.connector.http.base.model.auth.BearerAuthentication;
import io.camunda.connector.http.base.model.auth.OAuthAuthentication;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
Expand All @@ -46,7 +49,9 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;

Expand All @@ -60,6 +65,7 @@ public void shouldNotSetAuthentication_whenNotProvided() throws Exception {
// given request without authentication
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -74,6 +80,7 @@ public void shouldSetBasicAuthentication_whenProvided() throws Exception {
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setAuthentication(new BasicAuthentication("user", "password"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -89,6 +96,7 @@ public void shouldSetBearerAuthentication_whenProvided() throws Exception {
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setAuthentication(new BearerAuthentication("token"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -104,6 +112,7 @@ public void shouldSetOAuthAuthentication_whenProvided() throws Exception {
HttpCommonResult result = new HttpCommonResult(200, null, "{\"access_token\":\"token\"}");
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl("theurl");
request.setAuthentication(
new OAuthAuthentication(
"url", "clientId", "secret", "audience", OAuthConstants.CREDENTIALS_BODY, "scopes"));
Expand All @@ -128,6 +137,7 @@ public void shouldSetApiKeyAuthenticationInHeaders_whenProvided() throws Excepti
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setAuthentication(new ApiKeyAuthentication(ApiKeyLocation.HEADERS, "name", "value"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -142,6 +152,7 @@ public void shouldSetApiKeyAuthenticationInQuery_whenProvided() throws Exception
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setAuthentication(new ApiKeyAuthentication(ApiKeyLocation.QUERY, "name", "value"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -160,6 +171,7 @@ public void shouldNotSetQueryParameters_whenNotProvided() throws Exception {
// given request without query parameters
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -174,6 +186,7 @@ public void shouldSetQueryParameters_whenProvided() throws Exception {
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setQueryParameters(Map.of("key", "value"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -188,6 +201,7 @@ public void shouldSetQueryParameters_whenProvidedMultiple() throws Exception {
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setQueryParameters(Map.of("key", "value", "key2", "value2"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand Down Expand Up @@ -249,11 +263,66 @@ public void shouldSetUri_whenQueryHasParametersInUrl() throws Exception {
@Nested
class BodyTests {

private static Stream<Arguments> provideMultipartContentTypeHeaderWithWeirdCase() {
List<String> weirdContentTypes =
List.of("content-type", "ContEnt-TyPe", "CONTENT-TYPE", "Content-type");
List<String> weirdMultipart =
List.of(
"multipart/form-data",
"MULTIPART/FORM-DATA",
"MuLtIpArT/fOrM-dAtA",
ContentType.MULTIPART_FORM_DATA.toString(),
ContentType.MULTIPART_FORM_DATA.withCharset(StandardCharsets.UTF_8).toString());
List<String> combinedCases = new ArrayList<>();
for (String contentType : weirdContentTypes) {
for (String multipart : weirdMultipart) {
combinedCases.add(contentType);
combinedCases.add(multipart);
}
}
// combined values 2 by 2
List<Arguments> arguments = new ArrayList<>();
for (int i = 0; i < combinedCases.size(); i += 2) {
arguments.add(Arguments.of(combinedCases.get(i), combinedCases.get(i + 1)));
}

return arguments.stream();
}

private static Stream<Arguments> provideFormUrlEncodedContentTypeHeaderWithWeirdCase() {
List<String> weirdContentTypes =
List.of("content-type", "ContEnt-TyPe", "CONTENT-TYPE", "Content-type");
List<String> weirdFromUrlEncoded =
List.of(
"application/x-www-form-urlencodEd",
"APPLICATION/X-WWW-FORM-URLENCODED",
"AppLiCaTiOn/x-www-form-urlencoded",
ContentType.APPLICATION_FORM_URLENCODED.toString(),
ContentType.APPLICATION_FORM_URLENCODED
.withCharset(StandardCharsets.UTF_8)
.toString());
List<String> combinedCases = new ArrayList<>();
for (String contentType : weirdContentTypes) {
for (String formUrlEncoded : weirdFromUrlEncoded) {
combinedCases.add(contentType);
combinedCases.add(formUrlEncoded);
}
}
// combined values 2 by 2
List<Arguments> arguments = new ArrayList<>();
for (int i = 0; i < combinedCases.size(); i += 2) {
arguments.add(Arguments.of(combinedCases.get(i), combinedCases.get(i + 1)));
}

return arguments.stream();
}

@Test
public void shouldNotSetBody_whenBodyNotSupported() throws Exception {
// given request with body
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -268,6 +337,7 @@ public void shouldSetJsonBody_whenBodySupportedAndContentTypeNotProvided() throw
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
request.setBody(Map.of("key", "value"));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -293,6 +363,7 @@ public void shouldNotSetJsonBody_whenBodySupportedAndContentTypeProvided() throw
Map.of(
HttpHeaders.CONTENT_TYPE,
ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8).toString()));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -318,6 +389,7 @@ public void shouldSetJsonBody_whenBodySupportedAndContentTypeProvided() throws E
Map.of(
HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8).toString()));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -344,6 +416,7 @@ public void shouldSetJsonBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMa
Map.of(
HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8).toString()));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand Down Expand Up @@ -374,6 +447,7 @@ public void shouldSetJsonBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMa
ContentType.APPLICATION_FORM_URLENCODED
.withCharset(StandardCharsets.UTF_8)
.toString()));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand Down Expand Up @@ -408,6 +482,7 @@ public void shouldSetFormUrlEncodedBody_whenBodySupportedAndBodyIsMapAndHasNullV
ContentType.APPLICATION_FORM_URLENCODED
.withCharset(StandardCharsets.UTF_8)
.toString()));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -427,19 +502,16 @@ public void shouldSetFormUrlEncodedBody_whenBodySupportedAndBodyIsMapAndHasNullV
assertThat(content).contains("&");
}

@Test
public void shouldSetFormUrlEncodedBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMap()
throws Exception {
@ParameterizedTest
@MethodSource("provideFormUrlEncodedContentTypeHeaderWithWeirdCase")
public void shouldSetFormUrlEncodedBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMap(
String contentType, String formUrlEncodedValue) throws Exception {
// given request with body
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
request.setBody(Map.of("key", "value", "key2", "value2"));
request.setHeaders(
Map.of(
HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED
.withCharset(StandardCharsets.UTF_8)
.toString()));
request.setHeaders(Map.of(contentType, formUrlEncodedValue));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -458,16 +530,16 @@ public void shouldSetFormUrlEncodedBody_whenBodySupportedAndContentTypeProvidedA
assertThat(content).contains("&");
}

@Test
public void shouldSetMultipartBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMap() {
@ParameterizedTest
@MethodSource("provideMultipartContentTypeHeaderWithWeirdCase")
public void shouldSetMultipartBody_whenBodySupportedAndContentTypeProvidedAndBodyIsMap(
String contentType, String multipartValue) {
// given request with body
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
request.setBody(Map.of("key", "value", "key2", "value2"));
request.setHeaders(
Map.of(
HttpHeaders.CONTENT_TYPE,
ContentType.MULTIPART_FORM_DATA.withCharset(StandardCharsets.UTF_8).toString()));
request.setHeaders(Map.of(contentType, multipartValue));
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand Down Expand Up @@ -495,6 +567,7 @@ public void shouldSetContentType_whenNullProvidedAndPost(HttpMethod method)
headers.put(HttpHeaders.CONTENT_TYPE, null);
headers.put(HttpHeaders.ACCEPT, null);
headers.put("Other", null);
request.setUrl("theurl");
request.setHeaders(headers);

// when
Expand All @@ -514,6 +587,7 @@ public void shouldSetJsonContentType_WhenNotProvidedAndSupportsBody() throws Exc
// given request without headers
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -531,6 +605,7 @@ public void shouldSetJsonContentType_WhenNotProvidedAndSupportsBodyAndSomeHeader
HttpCommonRequest request = new HttpCommonRequest();
request.setHeaders(Map.of("Authorization", "Bearer token"));
request.setMethod(HttpMethod.POST);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -548,6 +623,7 @@ public void shouldNotSetJsonContentType_WhenNotProvidedAndDoesNotSupportBody()
// given request without headers
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand All @@ -563,6 +639,7 @@ public void shouldNotSetJsonContentType_WhenProvided() throws Exception {
HttpCommonRequest request = new HttpCommonRequest();
request.setHeaders(Map.of(HttpHeaders.CONTENT_TYPE, "text/plain"));
request.setMethod(HttpMethod.POST);
request.setUrl("theurl");

// when
ClassicHttpRequest httpRequest = ApacheRequestFactory.get().createHttpRequest(request);
Expand Down
Loading

0 comments on commit e07b88c

Please sign in to comment.