Skip to content

Commit 4a6fb6c

Browse files
RFC 3986 compatibility as optional for HttpApplicationSettings (prebid#4057)
1 parent 7e97ae7 commit 4a6fb6c

File tree

10 files changed

+389
-62
lines changed

10 files changed

+389
-62
lines changed

docs/config-app.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ For HTTP data source available next options:
346346
- `settings.http.amp-endpoint` - the url to fetch AMP stored requests.
347347
- `settings.http.video-endpoint` - the url to fetch video stored requests.
348348
- `settings.http.category-endpoint` - the url to fetch categories for long form video.
349+
- `settings.http.rfc3986-compatible` - if equals to `true` the url will be build according to RFC 3986, `false` by default
349350

350351
For account processing rules available next options:
351352
- `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejected with 401.

extra/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<commons.collections.version>4.4</commons.collections.version>
4040
<commons.compress.version>1.27.1</commons.compress.version>
4141
<commons-math3.version>3.6.1</commons-math3.version>
42+
<commons-validator.version>1.10.0</commons-validator.version>
4243
<scram.version>2.1</scram.version>
4344
<httpclient.version>4.5.14</httpclient.version>
4445
<ipaddress.version>5.5.1</ipaddress.version>
@@ -135,6 +136,11 @@
135136
<artifactId>commons-math3</artifactId>
136137
<version>${commons-math3.version}</version>
137138
</dependency>
139+
<dependency>
140+
<groupId>commons-validator</groupId>
141+
<artifactId>commons-validator</artifactId>
142+
<version>${commons-validator.version}</version>
143+
</dependency>
138144
<!-- TODO: refactor code to replace URIBuilder with something else so that this dep can be removed -->
139145
<dependency>
140146
<groupId>org.apache.httpcomponents</groupId>

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@
114114
<groupId>org.apache.httpcomponents</groupId>
115115
<artifactId>httpclient</artifactId>
116116
</dependency>
117+
<dependency>
118+
<groupId>commons-validator</groupId>
119+
<artifactId>commons-validator</artifactId>
120+
</dependency>
117121
<dependency>
118122
<groupId>com.github.seancfoley</groupId>
119123
<artifactId>ipaddress</artifactId>

src/main/java/org/prebid/server/settings/HttpApplicationSettings.java

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.apache.commons.collections4.CollectionUtils;
99
import org.apache.commons.collections4.MapUtils;
1010
import org.apache.commons.lang3.StringUtils;
11+
import org.apache.http.client.utils.URIBuilder;
1112
import org.prebid.server.exception.PreBidException;
1213
import org.prebid.server.execution.timeout.Timeout;
1314
import org.prebid.server.json.DecodeException;
@@ -25,6 +26,7 @@
2526
import org.prebid.server.vertx.httpclient.HttpClient;
2627
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
2728

29+
import java.net.URISyntaxException;
2830
import java.util.ArrayList;
2931
import java.util.Collections;
3032
import java.util.HashMap;
@@ -44,10 +46,14 @@
4446
* In order to enable caching and reduce latency for read operations {@link HttpApplicationSettings}
4547
* can be decorated by {@link CachingApplicationSettings}.
4648
* <p>
47-
* Expected the endpoint to satisfy the following API:
49+
* Expected the endpoint to satisfy the following API (URL is encoded):
4850
* <p>
4951
* GET {endpoint}?request-ids=["req1","req2"]&imp-ids=["imp1","imp2","imp3"]
5052
* <p>
53+
* or settings.http.rfc3986-compatible is set to true
54+
* <p>
55+
* * GET {endpoint}?request-id=req1&request-id=req2&imp-id=imp1&imp-id=imp2&imp-id=imp3
56+
* * <p>
5157
* This endpoint should return a payload like:
5258
* <pre>
5359
* {
@@ -76,20 +82,27 @@ public class HttpApplicationSettings implements ApplicationSettings {
7682
private final String categoryEndpoint;
7783
private final HttpClient httpClient;
7884
private final JacksonMapper mapper;
85+
private final boolean isRfc3986Compatible;
86+
87+
public HttpApplicationSettings(HttpClient httpClient,
88+
JacksonMapper mapper,
89+
String endpoint,
90+
String ampEndpoint,
91+
String videoEndpoint,
92+
String categoryEndpoint,
93+
boolean isRfc3986Compatible) {
7994

80-
public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, String endpoint, String ampEndpoint,
81-
String videoEndpoint, String categoryEndpoint) {
8295
this.httpClient = Objects.requireNonNull(httpClient);
8396
this.mapper = Objects.requireNonNull(mapper);
84-
this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint));
85-
this.ampEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(ampEndpoint));
86-
this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint));
87-
this.categoryEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(categoryEndpoint));
97+
this.endpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpoint));
98+
this.ampEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(ampEndpoint));
99+
this.videoEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(videoEndpoint));
100+
this.categoryEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(categoryEndpoint));
101+
this.isRfc3986Compatible = isRfc3986Compatible;
88102
}
89103

90104
@Override
91105
public Future<Account> getAccountById(String accountId, Timeout timeout) {
92-
93106
return fetchAccountsByIds(Collections.singleton(accountId), timeout)
94107
.map(accounts -> accounts.stream()
95108
.findFirst()
@@ -111,15 +124,20 @@ private Future<Set<Account>> fetchAccountsByIds(Set<String> accountIds, Timeout
111124
.recover(Future::failedFuture);
112125
}
113126

114-
private static String accountsRequestUrlFrom(String endpoint, Set<String> accountIds) {
115-
final StringBuilder url = new StringBuilder(endpoint);
116-
url.append(endpoint.contains("?") ? "&" : "?");
117-
118-
if (!accountIds.isEmpty()) {
119-
url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]");
127+
private String accountsRequestUrlFrom(String endpoint, Set<String> accountIds) {
128+
try {
129+
final URIBuilder uriBuilder = new URIBuilder(endpoint);
130+
if (!accountIds.isEmpty()) {
131+
if (isRfc3986Compatible) {
132+
accountIds.forEach(accountId -> uriBuilder.addParameter("account-id", accountId));
133+
} else {
134+
uriBuilder.addParameter("account-ids", "[\"%s\"]".formatted(joinIds(accountIds)));
135+
}
136+
}
137+
return uriBuilder.build().toString();
138+
} catch (URISyntaxException e) {
139+
throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
120140
}
121-
122-
return url.toString();
123141
}
124142

125143
private Future<Set<Account>> processAccountsResponse(HttpClientResponse response, Set<String> accountIds) {
@@ -165,9 +183,6 @@ public Future<StoredDataResult> getAmpStoredData(String accountId, Set<String> r
165183
return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout);
166184
}
167185

168-
/**
169-
* Not supported and returns failed result.
170-
*/
171186
@Override
172187
public Future<StoredDataResult> getVideoStoredData(String accountId, Set<String> requestIds, Set<String> impIds,
173188
Timeout timeout) {
@@ -240,22 +255,27 @@ private Future<StoredDataResult> fetchStoredData(String endpoint, Set<String> re
240255
.recover(exception -> failStoredDataResponse(exception, requestIds, impIds));
241256
}
242257

243-
private static String storeRequestUrlFrom(String endpoint, Set<String> requestIds, Set<String> impIds) {
244-
final StringBuilder url = new StringBuilder(endpoint);
245-
url.append(endpoint.contains("?") ? "&" : "?");
246-
247-
if (!requestIds.isEmpty()) {
248-
url.append("request-ids=[\"").append(joinIds(requestIds)).append("\"]");
249-
}
250-
251-
if (!impIds.isEmpty()) {
258+
private String storeRequestUrlFrom(String endpoint, Set<String> requestIds, Set<String> impIds) {
259+
try {
260+
final URIBuilder uriBuilder = new URIBuilder(endpoint);
252261
if (!requestIds.isEmpty()) {
253-
url.append("&");
262+
if (isRfc3986Compatible) {
263+
requestIds.forEach(requestId -> uriBuilder.addParameter("request-id", requestId));
264+
} else {
265+
uriBuilder.addParameter("request-ids", "[\"%s\"]".formatted(joinIds(requestIds)));
266+
}
267+
}
268+
if (!impIds.isEmpty()) {
269+
if (isRfc3986Compatible) {
270+
impIds.forEach(impId -> uriBuilder.addParameter("imp-id", impId));
271+
} else {
272+
uriBuilder.addParameter("imp-ids", "[\"%s\"]".formatted(joinIds(impIds)));
273+
}
254274
}
255-
url.append("imp-ids=[\"").append(joinIds(impIds)).append("\"]");
275+
return uriBuilder.build().toString();
276+
} catch (URISyntaxException e) {
277+
throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
256278
}
257-
258-
return url.toString();
259279
}
260280

261281
private static String joinIds(Set<String> ids) {

src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,17 @@ HttpApplicationSettings httpApplicationSettings(
122122
@Value("${settings.http.endpoint}") String endpoint,
123123
@Value("${settings.http.amp-endpoint}") String ampEndpoint,
124124
@Value("${settings.http.video-endpoint}") String videoEndpoint,
125-
@Value("${settings.http.category-endpoint}") String categoryEndpoint) {
125+
@Value("${settings.http.category-endpoint}") String categoryEndpoint,
126+
@Value("${settings.http.rfc3986-compatible:false}") boolean isRfc3986Compatible) {
126127

127-
return new HttpApplicationSettings(httpClient, mapper, endpoint, ampEndpoint, videoEndpoint,
128-
categoryEndpoint);
128+
return new HttpApplicationSettings(
129+
httpClient,
130+
mapper,
131+
endpoint,
132+
ampEndpoint,
133+
videoEndpoint,
134+
categoryEndpoint,
135+
isRfc3986Compatible);
129136
}
130137
}
131138

src/main/java/org/prebid/server/util/HttpUtil.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.vertx.core.http.HttpServerResponse;
99
import io.vertx.ext.web.RoutingContext;
1010
import org.apache.commons.lang3.StringUtils;
11+
import org.apache.commons.validator.routines.UrlValidator;
1112
import org.prebid.server.log.ConditionalLogger;
1213
import org.prebid.server.log.Logger;
1314
import org.prebid.server.log.LoggerFactory;
@@ -78,12 +79,15 @@ public final class HttpUtil {
7879
public static final String MACROS_OPEN = "{{";
7980
public static final String MACROS_CLOSE = "}}";
8081

82+
private static final UrlValidator URL_VALIDAROR = UrlValidator.getInstance();
83+
8184
private HttpUtil() {
8285
}
8386

8487
/**
8588
* Checks the input string for using as URL.
8689
*/
90+
@Deprecated
8791
public static String validateUrl(String url) {
8892
if (containsMacrosses(url)) {
8993
return url;
@@ -96,6 +100,14 @@ public static String validateUrl(String url) {
96100
}
97101
}
98102

103+
public static String validateUrlSyntax(String url) {
104+
if (containsMacrosses(url) || URL_VALIDAROR.isValid(url)) {
105+
return url;
106+
}
107+
108+
throw new IllegalArgumentException("URL supplied is not valid: " + url);
109+
}
110+
99111
// TODO: We need our own way to work with url macrosses
100112
private static boolean containsMacrosses(String url) {
101113
return StringUtils.contains(url, MACROS_OPEN) && StringUtils.contains(url, MACROS_CLOSE);

src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ class NetworkServiceContainer extends MockServerContainer {
88

99
NetworkServiceContainer(String version) {
1010
super(DockerImageName.parse("mockserver/mockserver:mockserver-$version"))
11+
def aliasWithTopLevelDomain = "${getNetworkAliases().first()}.com".toString()
12+
withCreateContainerCmdModifier { it.withHostName(aliasWithTopLevelDomain) }
13+
setNetworkAliases([aliasWithTopLevelDomain])
1114
}
1215

1316
String getHostAndPort() {

src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package org.prebid.server.functional.testcontainers.scaffolding
22

3+
import org.mockserver.matchers.Times
4+
import org.mockserver.model.Header
35
import org.mockserver.model.HttpRequest
6+
import org.mockserver.model.HttpStatusCode
7+
import org.prebid.server.functional.model.ResponseModel
48
import org.testcontainers.containers.MockServerContainer
59

610
import static org.mockserver.model.HttpRequest.request
11+
import static org.mockserver.model.HttpResponse.response
12+
import static org.mockserver.model.HttpStatusCode.OK_200
13+
import static org.mockserver.model.MediaType.APPLICATION_JSON
714

815
class HttpSettings extends NetworkScaffolding {
916

1017
private static final String ENDPOINT = "/stored-requests"
18+
private static final String RFC_ENDPOINT = "/stored-requests-rfc"
1119
private static final String AMP_ENDPOINT = "/amp-stored-requests"
1220

1321
HttpSettings(MockServerContainer mockServerContainer) {
@@ -27,12 +35,47 @@ class HttpSettings extends NetworkScaffolding {
2735

2836
@Override
2937
void setResponse() {
38+
}
39+
40+
protected HttpRequest getRfcRequest(String accountId) {
41+
request().withPath(RFC_ENDPOINT)
42+
.withQueryStringParameter("account-id", accountId)
43+
}
44+
45+
46+
void setRfcResponse(String value,
47+
ResponseModel responseModel,
48+
HttpStatusCode statusCode = OK_200,
49+
Map<String, String> headers = [:]) {
50+
def responseHeaders = headers.collect { new Header(it.key, it.value) }
51+
def mockResponse = encode(responseModel)
52+
mockServerClient.when(getRfcRequest(value), Times.unlimited())
53+
.respond(response().withStatusCode(statusCode.code())
54+
.withBody(mockResponse, APPLICATION_JSON)
55+
.withHeaders(responseHeaders))
56+
}
3057

58+
int getRfcRequestCount(String value) {
59+
mockServerClient.retrieveRecordedRequests(getRfcRequest(value))
60+
.size()
3161
}
3262

3363
@Override
3464
void reset() {
3565
super.reset(ENDPOINT)
66+
super.reset(RFC_ENDPOINT)
3667
super.reset(AMP_ENDPOINT)
3768
}
69+
70+
static String getEndpoint() {
71+
return ENDPOINT
72+
}
73+
74+
static String getAmpEndpoint() {
75+
return AMP_ENDPOINT
76+
}
77+
78+
static String getRfcEndpoint() {
79+
return RFC_ENDPOINT
80+
}
3881
}

0 commit comments

Comments
 (0)