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

Core: enable Sec-Browsing-Topics header on outgoing bidder requests #10340

Merged
merged 6 commits into from
Aug 11, 2023
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
6 changes: 1 addition & 5 deletions modules/geoedgeRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,4 @@ export const geoedgeSubmodule = {
onBidResponseEvent: conditionallyWrap
};

export function beforeInit() {
submodule('realTimeData', geoedgeSubmodule);
}

beforeInit();
submodule('realTimeData', geoedgeSubmodule);
10 changes: 8 additions & 2 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
deepAccess,
} from '../../src/utils.js';
import CONSTANTS from '../../src/constants.json';
import adapterManager from '../../src/adapterManager.js';
import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js';
import {config} from '../../src/config.js';
import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js';
import * as events from '../../src/events.js';
Expand All @@ -29,6 +29,8 @@ import {hook} from '../../src/hook.js';
import {hasPurpose1Consent} from '../../src/utils/gpdr.js';
import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js';
import {useMetrics} from '../../src/utils/perfMetrics.js';
import {isActivityAllowed} from '../../src/activities/rules.js';
import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js';

const getConfig = config.getConfig;

Expand Down Expand Up @@ -571,7 +573,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques
}
},
requestJson,
{contentType: 'text/plain', withCredentials: true}
{
contentType: 'text/plain',
withCredentials: true,
browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig))
}
);
} else {
logError('PBS request not made. Check endpoints.');
Expand Down
19 changes: 14 additions & 5 deletions src/adapters/bidderFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {useMetrics} from '../utils/perfMetrics.js';
import {isActivityAllowed} from '../activities/rules.js';
import {activityParams} from '../activities/activityParams.js';
import {MODULE_TYPE_BIDDER} from '../activities/modules.js';
import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js';
import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js';

/**
* This file aims to support Adapters during the Prebid 0.x -> 1.x transition.
Expand Down Expand Up @@ -454,6 +454,15 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe
onRequest(request);

const networkDone = requestMetrics.startTiming('net');

function getOptions(defaults) {
const ro = request.options;
return Object.assign(defaults, ro, {
browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics
? false
: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code))
})
}
switch (request.method) {
case 'GET':
ajax(
Expand All @@ -463,10 +472,10 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe
error: onFailure
},
undefined,
Object.assign({
getOptions({
method: 'GET',
withCredentials: true
}, request.options)
})
);
break;
case 'POST':
Expand All @@ -477,11 +486,11 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe
error: onFailure
},
typeof request.data === 'string' ? request.data : JSON.stringify(request.data),
Object.assign({
getOptions({
method: 'POST',
contentType: 'text/plain',
withCredentials: true
}, request.options)
})
);
break;
default:
Expand Down
213 changes: 126 additions & 87 deletions src/ajax.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,138 @@
import { config } from './config.js';
import { logMessage, logError, parseUrl, buildUrl, _each } from './utils.js';
import {config} from './config.js';
import {buildUrl, logError, parseUrl} from './utils.js';

const XHR_DONE = 4;
export const dep = {
fetch: window.fetch.bind(window),
makeRequest: (r, o) => new Request(r, o),
timeout(timeout, resource) {
const ctl = new AbortController();
let cancelTimer = setTimeout(() => {
ctl.abort();
logError(`Request timeout after ${timeout}ms`, resource);
cancelTimer = null;
}, timeout);
return {
signal: ctl.signal,
done() {
cancelTimer && clearTimeout(cancelTimer)
}
}
}
}

const GET = 'GET';
const POST = 'POST';
const CTYPE = 'Content-Type';

/**
* Simple IE9+ and cross-browser ajax request function
* Note: x-domain requests in IE9 do not support the use of cookies
*
* @param url string url
* @param callback {object | function} callback
* @param data mixed data
* @param options object
* transform legacy `ajax` parameters into a fetch request.
* @returns {Request}
*/
export const ajax = ajaxBuilder();

export function ajaxBuilder(timeout = 3000, {request, done} = {}) {
return function(url, callback, data, options = {}) {
try {
let x;
let method = options.method || (data ? 'POST' : 'GET');
let parser = document.createElement('a');
parser.href = url;

let callbacks = typeof callback === 'object' && callback !== null ? callback : {
success: function() {
logMessage('xhr success');
},
error: function(e) {
logError('xhr error', null, e);
}
};
export function toFetchRequest(url, data, options = {}) {
const method = options.method || (data ? POST : GET);
if (method === GET && data) {
const urlInfo = parseUrl(url, options);
Object.assign(urlInfo.search, data);
url = buildUrl(urlInfo);
}
const headers = new Headers(options.customHeaders);
headers.set(CTYPE, options.contentType || 'text/plain');
const rqOpts = {
method,
headers
}
if (method !== GET && data) {
rqOpts.body = data;
}
if (options.withCredentials) {
rqOpts.credentials = 'include';
}
if (options.browsingTopics && isSecureContext) {
// the Request constructor will throw an exception if the browser supports topics
// but we're not in a secure context
rqOpts.browsingTopics = true;
}
return dep.makeRequest(url, rqOpts);
}

if (typeof callback === 'function') {
callbacks.success = callback;
}
/**
* Return a version of `fetch` that automatically cancels requests after `timeout` milliseconds.
*
* If provided, `request` and `done` should be functions accepting a single argument.
* `request` is invoked at the beginning of each request, and `done` at the end; both are passed its origin.
*
* @returns {function(*, {}?): Promise<Response>}
*/
export function fetcherFactory(timeout = 3000, {request, done} = {}) {
let fetcher = (resource, options) => {
let to;
if (timeout != null && options?.signal == null && !config.getConfig('disableAjaxTimeout')) {
to = dep.timeout(timeout, resource);
options = Object.assign({signal: to.signal}, options);
}
let pm = dep.fetch(resource, options);
if (to?.done != null) pm = pm.finally(to.done);
return pm;
};

x = new window.XMLHttpRequest();
if (request != null || done != null) {
fetcher = ((fetch) => function (resource, options) {
const origin = new URL(resource?.url == null ? resource : resource.url, document.location).origin;
let req = fetch(resource, options);
request && request(origin);
if (done) req = req.finally(() => done(origin));
return req;
})(fetcher);
}
return fetcher;
}

x.onreadystatechange = function () {
if (x.readyState === XHR_DONE) {
if (typeof done === 'function') {
done(parser.origin);
}
let status = x.status;
if ((status >= 200 && status < 300) || status === 304) {
callbacks.success(x.responseText, x);
} else {
callbacks.error(x.statusText, x);
}
function toXHR({status, statusText = '', headers, url}, responseText) {
let xml = 0;
return {
readyState: XMLHttpRequest.DONE,
status,
statusText,
responseText,
response: responseText,
responseType: '',
responseURL: url,
get responseXML() {
if (xml === 0) {
try {
xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0])
} catch (e) {
xml = null;
logError(e);
}
};

// Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648
if (!config.getConfig('disableAjaxTimeout')) {
x.ontimeout = function () {
logError(' xhr timeout after ', x.timeout, 'ms');
};
}
return xml;
},
getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null,
}
}

if (method === 'GET' && data) {
let urlInfo = parseUrl(url, options);
Object.assign(urlInfo.search, data);
url = buildUrl(urlInfo);
}

x.open(method, url, true);
// IE needs timeout to be set after open - see #1410
// Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648
if (!config.getConfig('disableAjaxTimeout')) {
x.timeout = timeout;
}

if (options.withCredentials) {
x.withCredentials = true;
}
_each(options.customHeaders, (value, header) => {
x.setRequestHeader(header, value);
});
if (options.preflight) {
x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
x.setRequestHeader('Content-Type', options.contentType || 'text/plain');

if (typeof request === 'function') {
request(parser.origin);
}
/**
* attach legacy `ajax` callbacks to a fetch promise.
*/
export function attachCallbacks(fetchPm, callback) {
const {success, error} = typeof callback === 'object' && callback != null ? callback : {
success: typeof callback === 'function' ? callback : () => null,
error: (e, x) => logError('Network error', e, x)
};
fetchPm.then(response => response.text().then((responseText) => [response, responseText]))
.then(([response, responseText]) => {
const xhr = toXHR(response, responseText);
response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr);
}, () => error('', toXHR({status: 0}, '')));
}

if (method === 'POST' && data) {
x.send(data);
} else {
x.send();
}
} catch (error) {
logError('xhr construction', error);
typeof callback === 'object' && callback !== null && callback.error(error);
}
}
export function ajaxBuilder(timeout = 3000, {request, done} = {}) {
const fetcher = fetcherFactory(timeout, {request, done});
return function (url, callback, data, options = {}) {
attachCallbacks(fetcher(toFetchRequest(url, data, options)), callback);
};
}

export const ajax = ajaxBuilder();
export const fetch = fetcherFactory();
Loading