diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java new file mode 100644 index 00000000000..1e36609e93a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java @@ -0,0 +1,87 @@ +package org.prebid.server.bidder; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.MultiMap; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class HttpBidderRequestEnricher { + + private static final Set HEADERS_TO_COPY = Collections.singleton(HttpUtil.SEC_GPC_HEADER.toString()); + + private final String pbsRecord; + + public HttpBidderRequestEnricher(String pbsVersion) { + pbsRecord = createNameVersionRecord("pbs-java", Objects.requireNonNull(pbsVersion)); + } + + MultiMap enrichHeaders(MultiMap bidderRequestHeaders, MultiMap originalRequestHeaders, BidRequest bidRequest) { + // some bidders has headers on class level, so we create copy to not affect them + final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders); + + addOriginalRequestHeaders(originalRequestHeaders, bidderRequestHeadersCopy); + addXPrebidHeader(bidderRequestHeadersCopy, bidRequest); + + return bidderRequestHeadersCopy; + } + + private static MultiMap copyMultiMap(MultiMap source) { + final MultiMap copiedMultiMap = MultiMap.caseInsensitiveMultiMap(); + if (source != null && !source.isEmpty()) { + source.forEach(entry -> copiedMultiMap.add(entry.getKey(), entry.getValue())); + } + return copiedMultiMap; + } + + private static void addOriginalRequestHeaders(MultiMap originalHeaders, MultiMap bidderHeaders) { + originalHeaders.entries().stream() + .filter(entry -> HEADERS_TO_COPY.contains(entry.getKey()) + && !bidderHeaders.contains(entry.getKey())) + .forEach(entry -> bidderHeaders.add(entry.getKey(), entry.getValue())); + } + + private void addXPrebidHeader(MultiMap headers, BidRequest bidRequest) { + final String channelRecord = resolveChannelVersionRecord(bidRequest.getExt()); + final String sdkRecord = resolveSdkVersionRecord(bidRequest.getApp()); + final String value = Stream.of(channelRecord, sdkRecord, pbsRecord) + .filter(Objects::nonNull) + .collect(Collectors.joining(",")); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_PREBID_HEADER, value); + } + + private static String resolveChannelVersionRecord(ExtRequest extRequest) { + final ExtRequestPrebid extPrebid = extRequest != null ? extRequest.getPrebid() : null; + final ExtRequestPrebidChannel channel = extPrebid != null ? extPrebid.getChannel() : null; + final String channelName = channel != null ? channel.getName() : null; + final String channelVersion = channel != null ? channel.getVersion() : null; + + return createNameVersionRecord(channelName, channelVersion); + } + + private static String resolveSdkVersionRecord(App app) { + final ExtApp extApp = app != null ? app.getExt() : null; + final ExtAppPrebid extPrebid = extApp != null ? extApp.getPrebid() : null; + final String sdkSource = extPrebid != null ? extPrebid.getSource() : null; + final String sdkVersion = extPrebid != null ? extPrebid.getVersion() : null; + + return createNameVersionRecord(sdkSource, sdkVersion); + } + + private static String createNameVersionRecord(String name, String version) { + return StringUtils.isNotEmpty(name) && StringUtils.isNotEmpty(version) + ? String.format("%s/%s", name, version) + : null; + } +} diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 215ecfe7ac2..891e6184a44 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -5,7 +5,6 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; -import io.vertx.core.MultiMap; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; @@ -22,7 +21,6 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.execution.Timeout; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; -import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -32,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -50,19 +47,20 @@ public class HttpBidderRequester { private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class); - private static final Set HEADERS_TO_COPY = Collections.singleton(HttpUtil.SEC_GPC.toString()); - private final HttpClient httpClient; private final BidderRequestCompletionTrackerFactory completionTrackerFactory; private final BidderErrorNotifier bidderErrorNotifier; + private final HttpBidderRequestEnricher requestEnricher; public HttpBidderRequester(HttpClient httpClient, BidderRequestCompletionTrackerFactory completionTrackerFactory, - BidderErrorNotifier bidderErrorNotifier) { + BidderErrorNotifier bidderErrorNotifier, + HttpBidderRequestEnricher requestEnricher) { this.httpClient = Objects.requireNonNull(httpClient); this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory); this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier); + this.requestEnricher = Objects.requireNonNull(requestEnricher); } /** @@ -77,7 +75,8 @@ public Future requestBids(Bidder bidder, final Result>> httpRequestsWithErrors = bidder.makeHttpRequests(bidRequest); final List bidderErrors = httpRequestsWithErrors.getErrors(); - final List> httpRequests = enrichRequests(httpRequestsWithErrors.getValue(), routingContext); + final List> httpRequests = + enrichRequests(httpRequestsWithErrors.getValue(), routingContext, bidRequest); if (CollectionUtils.isEmpty(httpRequests)) { return emptyBidderSeatBidWithErrors(bidderErrors); @@ -108,34 +107,16 @@ public Future requestBids(Bidder bidder, .map(ignored -> resultBuilder.toBidderSeatBid(debugEnabled)); } - private static List> enrichRequests(List> httpRequests, - RoutingContext routingContext) { - + private List> enrichRequests(List> httpRequests, + RoutingContext routingContext, + BidRequest bidRequest) { return httpRequests.stream().map(httpRequest -> httpRequest.toBuilder() - .headers(enrichHeaders(httpRequest.getHeaders(), routingContext.request().headers())) + .headers(requestEnricher.enrichHeaders(httpRequest.getHeaders(), + routingContext.request().headers(), bidRequest)) .build()) .collect(Collectors.toList()); } - private static MultiMap enrichHeaders(MultiMap bidderRequestHeaders, MultiMap originalRequestHeaders) { - // some bidders has headers on class level, so we create copy to not affect them - final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders); - - originalRequestHeaders.entries().stream() - .filter(entry -> HEADERS_TO_COPY.contains(entry.getKey()) - && !bidderRequestHeadersCopy.contains(entry.getKey())) - .forEach(entry -> bidderRequestHeadersCopy.add(entry.getKey(), entry.getValue())); - return bidderRequestHeadersCopy; - } - - private static MultiMap copyMultiMap(MultiMap source) { - final MultiMap copiedMultiMap = MultiMap.caseInsensitiveMultiMap(); - if (source != null && !source.isEmpty()) { - source.forEach(entry -> copiedMultiMap.add(entry.getKey(), entry.getValue())); - } - return copiedMultiMap; - } - private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { if (StringUtils.isBlank(storedResponse)) { return false; diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 74444f530a4..a8f95969be0 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -32,6 +32,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; +import org.prebid.server.bidder.HttpBidderRequestEnricher; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.cache.CacheService; import org.prebid.server.cache.model.CacheTtl; @@ -462,9 +463,19 @@ BidderCatalog bidderCatalog(List bidderDeps) { HttpBidderRequester httpBidderRequester( HttpClient httpClient, @Autowired(required = false) BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory, - BidderErrorNotifier bidderErrorNotifier) { + BidderErrorNotifier bidderErrorNotifier, + HttpBidderRequestEnricher requestEnricher) { - return new HttpBidderRequester(httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier); + return new HttpBidderRequester(httpClient, + bidderRequestCompletionTrackerFactory, + bidderErrorNotifier, + requestEnricher); + } + + @Bean + HttpBidderRequestEnricher httpBidderRequestEnricher(VersionInfo versionInfo) { + + return new HttpBidderRequestEnricher(versionInfo.getVersion()); } @Bean diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java index c3f6b3278b1..2827bb172db 100644 --- a/src/main/java/org/prebid/server/util/HttpUtil.java +++ b/src/main/java/org/prebid/server/util/HttpUtil.java @@ -32,7 +32,7 @@ public final class HttpUtil { public static final CharSequence X_REQUEST_AGENT_HEADER = HttpHeaders.createOptimized("X-Request-Agent"); public static final CharSequence ORIGIN_HEADER = HttpHeaders.createOptimized("Origin"); public static final CharSequence ACCEPT_HEADER = HttpHeaders.createOptimized("Accept"); - public static final CharSequence SEC_GPC = HttpHeaders.createOptimized("Sec-GPC"); + public static final CharSequence SEC_GPC_HEADER = HttpHeaders.createOptimized("Sec-GPC"); public static final CharSequence CONTENT_TYPE_HEADER = HttpHeaders.createOptimized("Content-Type"); public static final CharSequence X_REQUESTED_WITH_HEADER = HttpHeaders.createOptimized("X-Requested-With"); public static final CharSequence REFERER_HEADER = HttpHeaders.createOptimized("Referer"); @@ -49,6 +49,7 @@ public final class HttpUtil { public static final CharSequence CONNECTION_HEADER = HttpHeaders.createOptimized("Connection"); public static final CharSequence ACCEPT_ENCODING_HEADER = HttpHeaders.createOptimized("Accept-Encoding"); public static final CharSequence X_OPENRTB_VERSION_HEADER = HttpHeaders.createOptimized("x-openrtb-version"); + public static final CharSequence X_PREBID_HEADER = HttpHeaders.createOptimized("x-prebid"); private HttpUtil() { } diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java new file mode 100644 index 00000000000..a9cb92bbd58 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java @@ -0,0 +1,103 @@ +package org.prebid.server.bidder; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.MultiMap; +import io.vertx.core.http.CaseInsensitiveHeaders; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HttpBidderRequestEnricherTest { + + private HttpBidderRequestEnricher requestEnricher; + + @Before + public void setUp() { + + requestEnricher = new HttpBidderRequestEnricher("1.00"); + } + + @Test + public void shouldSendPopulatedPostRequest() { + // given + final MultiMap headers = new CaseInsensitiveHeaders(); + headers.add("header1", "value1"); + headers.add("header2", "value2"); + + // when + final MultiMap resultHeaders = + requestEnricher.enrichHeaders(headers, new CaseInsensitiveHeaders(), BidRequest.builder().build()); + + // then + final MultiMap expectedHeaders = new CaseInsensitiveHeaders(); + expectedHeaders.addAll(headers); + expectedHeaders.add("x-prebid", "pbs-java/1.00"); + assertThat(resultHeaders).hasSize(3); + assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue(); + } + + @Test + public void shouldAddSecGpcHeaderFromOriginalRequest() { + // given + final MultiMap originalHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "1"); + + // when + final MultiMap resultHeaders = + requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(), + originalHeaders, BidRequest.builder().build()); + + // then + assertThat(resultHeaders.contains("Sec-GPC")).isTrue(); + assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("1"); + } + + @Test + public void shouldNotOverrideHeadersFromBidRequest() { + // given + final MultiMap originalHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "1"); + final MultiMap bidderRequestHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "0"); + + // when + final MultiMap resultHeaders = requestEnricher.enrichHeaders(bidderRequestHeaders, + originalHeaders, BidRequest.builder().build()); + + // then + assertThat(resultHeaders.contains("Sec-GPC")).isTrue(); + assertThat(resultHeaders.getAll("Sec-GPC")).hasSize(1); + assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("0"); + } + + @Test + public void shouldCreateXPrebidHeaderForOutgoingRequest() { + // given + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("pbjs", "4.39")) + .build())) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final MultiMap resultHeaders = requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(), + new CaseInsensitiveHeaders(), bidRequest); + + // then + final MultiMap expectedHeaders = new CaseInsensitiveHeaders(); + expectedHeaders.add("x-prebid", "pbjs/4.39,prebid-mobile/1.2.3,pbs-java/1.00"); + assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue(); + } + + private static boolean isEqualsMultiMaps(MultiMap left, MultiMap right) { + return left.size() == right.size() && left.entries().stream() + .allMatch(entry -> right.contains(entry.getKey(), entry.getValue(), true)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java index 3a8eb9daca8..0dfd7b8edaa 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java @@ -45,8 +45,6 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.anyLong; @@ -56,6 +54,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; public class HttpBidderRequesterTest extends VertxTest { @@ -69,6 +68,8 @@ public class HttpBidderRequesterTest extends VertxTest { @Mock private BidderErrorNotifier bidderErrorNotifier; @Mock + private HttpBidderRequestEnricher requestEnricher; + @Mock private RoutingContext routingContext; @Mock private HttpServerRequest httpServerRequest; @@ -85,13 +86,14 @@ public void setUp() { given(bidderErrorNotifier.processTimeout(any(), any())).will(invocation -> invocation.getArgument(0)); given(routingContext.request()).willReturn(httpServerRequest); given(httpServerRequest.headers()).willReturn(new CaseInsensitiveHeaders()); + given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(new CaseInsensitiveHeaders()); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); timeout = timeoutFactory.create(500L); expiredTimeout = timeoutFactory.create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L); - httpBidderRequester = new HttpBidderRequester(httpClient, null, bidderErrorNotifier); + httpBidderRequester = new HttpBidderRequester(httpClient, null, bidderErrorNotifier, requestEnricher); } @Test @@ -130,34 +132,6 @@ public void shouldTolerateBidderReturningErrorsAndNoHttpRequests() { .extracting(BidderError::getMessage).containsOnly("error1", "error2"); } - @Test - public void shouldSendPopulatedPostRequest() { - // given - givenHttpClientReturnsResponse(200, null); - - final MultiMap headers = new CaseInsensitiveHeaders(); - headers.add("header1", "value1"); - headers.add("header2", "value2"); - - given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri("uri") - .body("requestBody") - .headers(headers) - .build()), - emptyList())); - - final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build()); - - // when - httpBidderRequester.requestBids(bidder, bidderRequest, timeout, routingContext, false); - - // then - verify(httpClient).request(eq(HttpMethod.POST), eq("uri"), argThat(new MultiMapMatcher(headers)), - eq("requestBody"), eq(500L)); - } - @Test public void shouldPassStoredResponseToBidderMakeBidsMethodAndReturnSeatBids() { // given @@ -386,57 +360,6 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() { ExtHttpCall.builder().uri("uri1").requestbody("requestBody1").build()); } - @Test - public void shouldAddSecGpcHeaderFromOriginalRequest() { - // given - givenHttpClientReturnsResponse(200, null); - given(httpServerRequest.headers()).willReturn(new CaseInsensitiveHeaders().add("Sec-GPC", "1")); - - given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri("uri") - .body("requestBody") - .headers(new CaseInsensitiveHeaders()) - .build()), - emptyList())); - - final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build()); - // when - httpBidderRequester.requestBids(bidder, bidderRequest, timeout, routingContext, false); - - // then - verify(httpClient).request(eq(HttpMethod.POST), eq("uri"), headerCaptor.capture(), eq("requestBody"), eq(500L)); - assertThat(headerCaptor.getValue().contains("Sec-GPC")).isTrue(); - assertThat(headerCaptor.getValue().get("Sec-GPC")).isEqualTo("1"); - } - - @Test - public void shouldNotOverrideHeadersFromBidRequest() { - // given - givenHttpClientReturnsResponse(200, null); - given(httpServerRequest.headers()).willReturn(new CaseInsensitiveHeaders().add("Sec-GPC", "1")); - - given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri("uri") - .body("requestBody") - .headers(new CaseInsensitiveHeaders().add("Sec-GPC", "0")) - .build()), - emptyList())); - - final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build()); - // when - httpBidderRequester.requestBids(bidder, bidderRequest, timeout, routingContext, false); - - // then - verify(httpClient).request(eq(HttpMethod.POST), eq("uri"), headerCaptor.capture(), eq("requestBody"), eq(500L)); - assertThat(headerCaptor.getValue().contains("Sec-GPC")).isTrue(); - assertThat(headerCaptor.getValue().getAll("Sec-GPC")).hasSize(1); - assertThat(headerCaptor.getValue().get("Sec-GPC")).isEqualTo("0"); - } - @Test public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() { // given @@ -562,7 +485,7 @@ public void shouldTolerateMultipleErrors() { .headers(new CaseInsensitiveHeaders()) .build()), singletonList(BidderError.badInput("makeHttpRequestsError")))); - + when(requestEnricher.enrichHeaders(any(), any(), any())).thenAnswer(invocation -> new CaseInsensitiveHeaders()); given(httpClient.request(any(), anyString(), any(), anyString(), anyLong())) // simulate response error for the first request .willReturn(Future.failedFuture(new RuntimeException("Response exception")))