From e90e12289050f31b97eaa6c1cdeeea714bd6a44d Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Wed, 13 Apr 2022 19:59:58 +0530 Subject: [PATCH 1/7] Fixed bugs on rtd module and added the entropy values required by Akamai DAP --- .../gpt/akamaidap_segments_example.html | 6 +- modules/.submodules.json | 1 + modules/akamaiDapRtdProvider.js | 751 +++++++++++++++--- modules/akamaiDapRtdProvider.md | 5 +- .../spec/modules/akamaiDapRtdProvider_spec.js | 467 ++++++++--- 5 files changed, 991 insertions(+), 239 deletions(-) diff --git a/integrationExamples/gpt/akamaidap_segments_example.html b/integrationExamples/gpt/akamaidap_segments_example.html index e85ac8e1337..450633ed0db 100644 --- a/integrationExamples/gpt/akamaidap_segments_example.html +++ b/integrationExamples/gpt/akamaidap_segments_example.html @@ -68,6 +68,7 @@ } }, realTimeData: { + auctionDelay: 2000, dataProviders: [ { name: "dap", @@ -76,9 +77,8 @@ apiHostname: "prebid.dap.akadns.net", apiVersion: "x1", domain: "prebid.org", - identityType: "dap-signature:1.0.0", - segtax: 503, - tokenTtl: 5, + identityType: "dap-signature:1.3.0", + segtax: 504 } } ] diff --git a/modules/.submodules.json b/modules/.submodules.json index 85e4658cc61..43c5e396c60 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -48,6 +48,7 @@ "dfpAdServerVideo" ], "rtdModule": [ + "akamaiDapRtdProvider", "browsiRtdProvider", "dgkeywordRtdProvider", "geoedgeRtdProvider", diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index aca984d39c8..0d02d400073 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -4,24 +4,35 @@ * The module will fetch real-time data from DAP * @module modules/akamaiDapRtdProvider * @requires module:modules/realTimeData - */ +*/ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; +import sha256 from 'crypto-js/sha256'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; -export const SEGMENTS_STORAGE_KEY = 'akamaiDapSegments'; +export const DAP_TOKEN = 'async_dap_token'; +export const DAP_MEMBERSHIP = 'async_dap_membership'; +export const DAP_ENCRYPTED_MEMBERSHIP = 'encrypted_dap_membership'; +export const DAP_SS_ID = 'dap_ss_id'; +export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds +export const DAP_MAX_RETRY_TOKENIZE = 1; +export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' +export const DAP_AUDIO_FP = 'dap_e17' +export const ENTROPY_EXPIRY = 3600; // in seconds + export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); +let dapRetryTokenize = 0; /** * Lazy merge objects. * @param {String} target * @param {String} source - */ +*/ function mergeLazy(target, source) { if (!isPlainObject(target)) { target = {}; @@ -53,60 +64,68 @@ export function addRealTimeData(rtd) { * Real-time data retrieval from Audigent * @param {Object} reqBidsConfigObj * @param {function} onDone - * @param {Object} rtdConfi + * @param {Object} rtdConfig * @param {Object} userConsent */ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { + let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); + if (entropyDict && entropyDict.expires_at > Math.round(Date.now() / 1000.0)) { + logMessage('Using cached entropy'); + } else { + dapUtils.dapCalculateEntropy(); + } logInfo('DEBUG(getRealTimeData) - ENTER'); logMessage(' - apiHostname: ' + rtdConfig.params.apiHostname); logMessage(' - apiVersion: ' + rtdConfig.params.apiVersion); - let jsonData = storage.getDataFromLocalStorage(SEGMENTS_STORAGE_KEY); + dapRetryTokenize = 0; + var jsonData = null; + if (rtdConfig && isPlainObject(rtdConfig.params)) { + if (rtdConfig.params.segtax == 504) { + let encMembership = dapUtils.dapGetEncryptedMembershipFromLocalStorage(); + if (encMembership) { + jsonData = dapUtils.dapGetEncryptedRtdObj(encMembership, rtdConfig.params.segtax) + } + } else { + let membership = dapUtils.dapGetMembershipFromLocalStorage(); + if (membership) { + jsonData = dapUtils.dapGetRtdObj(membership, rtdConfig.params.segtax) + } + } + } if (jsonData) { - let data = JSON.parse(jsonData); - if (data.rtd) { - addRealTimeData(data.rtd); + if (jsonData.rtd) { + addRealTimeData(jsonData.rtd); onDone(); logInfo('DEBUG(getRealTimeData) - 1'); // Don't return - ensure the data is always fresh. } } + // Calling setTimeout to release the main thread so that the bid request could be sent. + setTimeout(callDapAPIs, 0, bidConfig, onDone, rtdConfig, userConsent); +} +function callDapAPIs(bidConfig, onDone, rtdConfig, userConsent) { if (rtdConfig && isPlainObject(rtdConfig.params)) { let config = { api_hostname: rtdConfig.params.apiHostname, api_version: rtdConfig.params.apiVersion, domain: rtdConfig.params.domain, - segtax: rtdConfig.params.segtax + segtax: rtdConfig.params.segtax, + identity: {type: rtdConfig.params.identityType} }; - let identity = { - type: rtdConfig.params.identityType - }; - let token = dapUtils.dapGetToken(config, identity, rtdConfig.params.tokenTtl); - if (token !== null) { - let membership = dapUtils.dapGetMembership(config, token); - let udSegment = dapUtils.dapMembershipToRtbSegment(membership, config); - logMessage('DEBUG(getRealTimeData) - token: ' + token + ', user.data.segment: ', udSegment); - let data = { - rtd: { - ortb2: { - user: { - data: [ - udSegment - ] - }, - site: { - ext: { - data: { - dapSAID: membership.said - } - } - } - } - } - }; - storage.setDataInLocalStorage(SEGMENTS_STORAGE_KEY, JSON.stringify(data)); - onDone(); + let refreshMembership = true; + let token = dapUtils.dapGetTokenFromLocalStorage(); + logMessage('token is: ', token); + if (token !== null) { // If token is not null then check the membership in storage and add the RTD object + if (config.segtax == 504) { // Follow the encrypted membership path + dapUtils.dapRefreshEncryptedMembership(config, token, onDone) // Get the encrypted membership from server + refreshMembership = false; + } else { + dapUtils.dapRefreshMembership(config, token, onDone) // Get the membership from server + refreshMembership = false; + } } + dapUtils.dapRefreshToken(config, refreshMembership, onDone) // Refresh Token and membership in all the cases } } @@ -115,7 +134,7 @@ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { * @param {Object} provider * @param {Object} userConsent * @return {boolean} - */ +*/ function init(provider, userConsent) { return true; } @@ -131,101 +150,406 @@ submodule(MODULE_NAME, akamaiDapRtdSubmodule); export const dapUtils = { - dapGetToken: function(config, identity, ttl) { - let now = Math.round(Date.now() / 1000.0); // in seconds - let storageName = 'async_dap_token'; - let token = null; + dapCalculateEntropy: function() { + dapUtils.dapGetAudioFp(); + let entropyDict = {} + let entropy = {} + // canvas fp + entropy.e1 = dapUtils.dapGetCanvasFp(); + // fonts fp + entropy.e2 = dapUtils.dapGetFontsFp(); + // webgl fp + entropy.e3 = dapUtils.dapGetWebglFp(); + // misc fp + entropy.e4 = window.screen.colorDepth ? JSON.stringify(window.screen.colorDepth) : 'NA'; + entropy.e5 = navigator.deviceMemory ? JSON.stringify(navigator.deviceMemory) : 'NA'; + entropy.e6 = navigator.cpuClass ? navigator.cpuClass : 'NA' + entropy.e7 = navigator.language ? navigator.language : 'NA'; + entropy.e8 = navigator.cookieEnabled ? JSON.stringify(navigator.cookieEnabled) : 'NA'; + entropy.e9 = navigator.userAgent ? navigator.userAgent : 'NA'; + entropy.e10 = navigator.geoLocation ? navigator.geoLocation : 'NA'; + entropy.e11 = navigator.hardwareConcurrency ? JSON.stringify(navigator.hardwareConcurrency) : 'NA'; + entropy.e12 = window.indexedDB ? JSON.stringify(window.indexedDB) : 'NA'; + entropy.e13 = window.openDatabase ? JSON.stringify(window.openDatabase.length) : 'NA'; + entropy.e14 = navigator.ipAddress ? navigator.ipAddress : 'NA'; + entropy.e15 = navigator.platform ? navigator.platform : 'NA'; + var len = navigator.plugins.length; + var plugins = [] + for (var i = 0; i < len; i++) { + plugins.push(navigator.plugins[i].name); + } + entropy.e16 = sha256(JSON.stringify(plugins, Object.keys(plugins).sort())).toString() || 'NA'; + var pluginsSorted = plugins.sort() + entropy.e18 = sha256(JSON.stringify(pluginsSorted, Object.keys(pluginsSorted).sort())).toString() || 'NA'; + + // Add entropy values along with the expiry to entropyDict and store in localstorage. + entropyDict.entropy = entropy + entropyDict.expires_at = Math.round(Date.now() / 1000.0) + ENTROPY_EXPIRY; + localStorage.setItem(DAP_CLIENT_ENTROPY, JSON.stringify(entropyDict)); + }, + + dapGetCanvasFp: function() { + var strOnError, canvas, strCText, strText, strOut; + + strOnError = 'Error_canvas'; + canvas = null; + strCText = null; + strText = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?"; + strOut = null; + + try { + canvas = document.createElement('canvas'); + strCText = canvas.getContext('2d'); + strCText.textBaseline = 'top'; + strCText.font = "14px 'Arial'"; + strCText.textBaseline = 'alphabetic'; + strCText.fillStyle = '#f60'; + strCText.fillRect(125, 1, 62, 20); + strCText.fillStyle = '#069'; + strCText.fillText(strText, 2, 15); + strCText.fillStyle = 'rgba(102, 204, 0, 0.7)'; + strCText.fillText(strText, 4, 17); + strOut = canvas.toDataURL(); + const hashCanvas = sha256(strOut); + + return hashCanvas.toString(); + } catch (err) { + logMessage('Error while calculating dapGetCanvasFp() ', strOnError); + } + }, + + dapGetFontsFp: function() { + var strOnError, style, fonts, count, template, fragment, divs, i, font, div, body, result, e; + + strOnError = 'Error_fonts'; + style = null; + fonts = null; + font = null; + count = 0; + template = null; + divs = null; + e = null; + div = null; + body = null; + i = 0; + + try { + style = 'position: absolute; visibility: hidden; display: block !important'; + fonts = ['Abadi MT Condensed Light', 'Adobe Fangsong Std', 'Adobe Hebrew', 'Adobe Ming Std', 'Agency FB', 'Aharoni', 'Andalus', 'Angsana New', 'AngsanaUPC', 'Aparajita', 'Arab', 'Arabic Transparent', 'Arabic Typesetting', 'Arial Baltic', 'Arial Black', 'Arial CE', 'Arial CYR', 'Arial Greek', 'Arial TUR', 'Arial', 'Batang', 'BatangChe', 'Bauhaus 93', 'Bell MT', 'Bitstream Vera Serif', 'Bodoni MT', 'Bookman Old Style', 'Braggadocio', 'Broadway', 'Browallia New', 'BrowalliaUPC', 'Calibri Light', 'Calibri', 'Californian FB', 'Cambria Math', 'Cambria', 'Candara', 'Castellar', 'Casual', 'Centaur', 'Century Gothic', 'Chalkduster', 'Colonna MT', 'Comic Sans MS', 'Consolas', 'Constantia', 'Copperplate Gothic Light', 'Corbel', 'Cordia New', 'CordiaUPC', 'Courier New Baltic', 'Courier New CE', 'Courier New CYR', 'Courier New Greek', 'Courier New TUR', 'Courier New', 'DFKai-SB', 'DaunPenh', 'David', 'DejaVu LGC Sans Mono', 'Desdemona', 'DilleniaUPC', 'DokChampa', 'Dotum', 'DotumChe', 'Ebrima', 'Engravers MT', 'Eras Bold ITC', 'Estrangelo Edessa', 'EucrosiaUPC', 'Euphemia', 'Eurostile', 'FangSong', 'Forte', 'FrankRuehl', 'Franklin Gothic Heavy', 'Franklin Gothic Medium', 'FreesiaUPC', 'French Script MT', 'Gabriola', 'Gautami', 'Georgia', 'Gigi', 'Gisha', 'Goudy Old Style', 'Gulim', 'GulimChe', 'GungSeo', 'Gungsuh', 'GungsuhChe', 'Haettenschweiler', 'Harrington', 'Hei S', 'HeiT', 'Heisei Kaku Gothic', 'Hiragino Sans GB', 'Impact', 'Informal Roman', 'IrisUPC', 'Iskoola Pota', 'JasmineUPC', 'KacstOne', 'KaiTi', 'Kalinga', 'Kartika', 'Khmer UI', 'Kino MT', 'KodchiangUPC', 'Kokila', 'Kozuka Gothic Pr6N', 'Lao UI', 'Latha', 'Leelawadee', 'Levenim MT', 'LilyUPC', 'Lohit Gujarati', 'Loma', 'Lucida Bright', 'Lucida Console', 'Lucida Fax', 'Lucida Sans Unicode', 'MS Gothic', 'MS Mincho', 'MS PGothic', 'MS PMincho', 'MS Reference Sans Serif', 'MS UI Gothic', 'MV Boli', 'Magneto', 'Malgun Gothic', 'Mangal', 'Marlett', 'Matura MT Script Capitals', 'Meiryo UI', 'Meiryo', 'Menlo', 'Microsoft Himalaya', 'Microsoft JhengHei', 'Microsoft New Tai Lue', 'Microsoft PhagsPa', 'Microsoft Sans Serif', 'Microsoft Tai Le', 'Microsoft Uighur', 'Microsoft YaHei', 'Microsoft Yi Baiti', 'MingLiU', 'MingLiU-ExtB', 'MingLiU_HKSCS', 'MingLiU_HKSCS-ExtB', 'Miriam Fixed', 'Miriam', 'Mongolian Baiti', 'MoolBoran', 'NSimSun', 'Narkisim', 'News Gothic MT', 'Niagara Solid', 'Nyala', 'PMingLiU', 'PMingLiU-ExtB', 'Palace Script MT', 'Palatino Linotype', 'Papyrus', 'Perpetua', 'Plantagenet Cherokee', 'Playbill', 'Prelude Bold', 'Prelude Condensed Bold', 'Prelude Condensed Medium', 'Prelude Medium', 'PreludeCompressedWGL Black', 'PreludeCompressedWGL Bold', 'PreludeCompressedWGL Light', 'PreludeCompressedWGL Medium', 'PreludeCondensedWGL Black', 'PreludeCondensedWGL Bold', 'PreludeCondensedWGL Light', 'PreludeCondensedWGL Medium', 'PreludeWGL Black', 'PreludeWGL Bold', 'PreludeWGL Light', 'PreludeWGL Medium', 'Raavi', 'Rachana', 'Rockwell', 'Rod', 'Sakkal Majalla', 'Sawasdee', 'Script MT Bold', 'Segoe Print', 'Segoe Script', 'Segoe UI Light', 'Segoe UI Semibold', 'Segoe UI Symbol', 'Segoe UI', 'Shonar Bangla', 'Showcard Gothic', 'Shruti', 'SimHei', 'SimSun', 'SimSun-ExtB', 'Simplified Arabic Fixed', 'Simplified Arabic', 'Snap ITC', 'Sylfaen', 'Symbol', 'Tahoma', 'Times New Roman Baltic', 'Times New Roman CE', 'Times New Roman CYR', 'Times New Roman Greek', 'Times New Roman TUR', 'Times New Roman', 'TlwgMono', 'Traditional Arabic', 'Trebuchet MS', 'Tunga', 'Tw Cen MT Condensed Extra Bold', 'Ubuntu', 'Umpush', 'Univers', 'Utopia', 'Utsaah', 'Vani', 'Verdana', 'Vijaya', 'Vladimir Script', 'Vrinda', 'Webdings', 'Wide Latin', 'Wingdings']; + count = fonts.length; + template = 'ww' + 'ww'; + fragment = document.createDocumentFragment(); + divs = []; + for (i = 0; i < count; i = i + 1) { + font = fonts[i]; + div = document.createElement('div'); + font = font.replace(/['"<>]/g, ''); + div.innerHTML = template.replace(/X/g, font); + div.style.cssText = style; + fragment.appendChild(div); + divs.push(div); + } + body = document.body; + body.insertBefore(fragment, body.firstChild); + result = []; + for (i = 0; i < count; i = i + 1) { + e = divs[i].getElementsByTagName('b'); + if (e[0].offsetWidth === e[1].offsetWidth) { + result.push(fonts[i]); + } + } + // do not combine these two loops, remove child will cause reflow + // and induce severe performance hit + for (i = 0; i < count; i = i + 1) { + body.removeChild(divs[i]); + } + const hashFonts = sha256(result.join('|')); + return hashFonts.toString(); + } catch (err) { + logMessage('Error while calcualting dapGetFontsFp() ' + strOnError); + } + }, + + dapGetWebglFp: function() { + var canvas, webglContext; + var width = 48; + var height = 27; + var webglString = ''; + try { + canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + webglContext = canvas.getContext('webgl2') || canvas.getContext('experimental-webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl') || canvas.getContext('moz-webgl'); + } catch (e) { + logMessage('Exeception occured: ', e); + return; + } + + try { + if (webglContext) { + var webglBuffer = webglContext.createBuffer(); + webglContext.bindBuffer(webglContext.ARRAY_BUFFER, webglBuffer); + + var size = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]); + webglContext.bufferData(webglContext.ARRAY_BUFFER, size, webglContext.STATIC_DRAW); + webglBuffer.itemSize = 3; + webglBuffer.numItems = 3; + var webglProgram = webglContext.createProgram(); + var vertexShader = webglContext.createShader(webglContext.VERTEX_SHADER); + var vertexShaderSource = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'; + webglContext.shaderSource(vertexShader, vertexShaderSource); + webglContext.compileShader(vertexShader); + + var fragmentShader = webglContext.createShader(webglContext.FRAGMENT_SHADER); + var fragmentShaderSource = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'; + webglContext.shaderSource(fragmentShader, fragmentShaderSource); + webglContext.compileShader(fragmentShader); + webglContext.attachShader(webglProgram, vertexShader); + webglContext.attachShader(webglProgram, fragmentShader); + webglContext.linkProgram(webglProgram); + webglContext.useProgram(webglProgram); + webglProgram.vertexPosAttrib = webglContext.getAttribLocation(webglProgram, 'attrVertex'); + webglProgram.offsetUniform = webglContext.getUniformLocation(webglProgram, 'uniformOffset'); + webglContext.enableVertexAttribArray(webglProgram.vertexPosArray); + webglContext.vertexAttribPointer(webglProgram.vertexPosAttrib, webglBuffer.itemSize, webglContext.FLOAT, !1, 0, 0); + webglContext.uniform2f(webglProgram.offsetUniform, 1, 1); + webglContext.drawArrays(webglContext.TRIANGLE_STRIP, 0, webglBuffer.numItems); + } + } catch (e) { + logMessage('Exeception occured: ', e); + } + + var pixels = new Uint8Array(width * height * 4); + webglContext.readPixels(0, 0, width, height, webglContext.RGBA, webglContext.UNSIGNED_BYTE, pixels); + webglString = JSON.stringify(pixels).replace(/,?"[0-9]+":/g, ''); + logMessage('webgl fp', webglString); + const hashWebgl = sha256(webglString); + return hashWebgl.toString(); + }, + + dapGetAudioFp: function(entropy) { + var context = null; + var currentTime = null; + var oscillator = null; + var compressor = null; + + try { + var AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext; + context = new AudioContext(1, 44100, 44100); + currentTime = context.currentTime; + oscillator = context.createOscillator(); + oscillator.type = 'triangle'; + oscillator.frequency.setValueAtTime(10000, currentTime); + + compressor = context.createDynamicsCompressor() + compressor.threshold.value = -50 + compressor.knee.value = 40 + compressor.ratio.value = 12 + compressor.attack.value = 0 + compressor.release.value = 0.25 + + oscillator.connect(compressor); + compressor.connect(context.destination); + + oscillator.start(0); + context.startRendering(); + + context.oncomplete = dapUtils.dapOnCompleteCallback; + } catch (e) { + logMessage('error', e); + } + }, - if (ttl == 0) { - localStorage.removeItem(storageName); + dapOnCompleteCallback: function(event) { + var output = null; + for (var i = 4500; i < 5e3; i++) { + var channelData = event.renderedBuffer.getChannelData(0)[i]; + output += Math.abs(channelData); } - let item = JSON.parse(localStorage.getItem(storageName)); - if (item == null) { - item = { - expires_at: now - 1, - token: null - }; + let fingerprint = output.toString(); + if (fingerprint) { + localStorage.setItem(DAP_AUDIO_FP, JSON.stringify(fingerprint)); } else { - token = item.token; - } - - if (now > item.expires_at) { - dapUtils.dapLog('Token missing or expired, fetching a new one...'); - // Trigger a refresh - let configAsync = {...config}; - dapUtils.dapTokenize(configAsync, identity, - function(token, status, xhr) { - item.expires_at = now + ttl; - item.token = token; - localStorage.setItem(storageName, JSON.stringify(item)); - dapUtils.dapLog('Successfully updated and stored token; expires in ' + ttl + ' seconds'); - let deviceId100 = xhr.getResponseHeader('Akamai-DAP-100'); - if (deviceId100 != null) { - localStorage.setItem('dap_deviceId100', deviceId100); - dapUtils.dapLog('Successfully stored DAP 100 Device ID: ' + deviceId100); - } - }, - function(xhr, status, error) { - logError('ERROR(' + error + '): failed to retrieve token! ' + status); - } - ); + localStorage.setItem(DAP_AUDIO_FP, 'NC'); } + }, + dapGetTokenFromLocalStorage: function(ttl) { + let now = Math.round(Date.now() / 1000.0); // in seconds + let token = null; + let item = JSON.parse(localStorage.getItem(DAP_TOKEN)); + if (item) { + if (now < item.expires_at) { + token = item.token; + } + } return token; }, - dapGetMembership: function(config, token) { + dapRefreshToken: function(config, refreshMembership, onDone) { + dapUtils.dapLog('Token missing or expired, fetching a new one...'); + // Trigger a refresh + let now = Math.round(Date.now() / 1000.0); // in seconds + let item = {} + let configAsync = {...config}; + dapUtils.dapTokenize(configAsync, config.identity, onDone, + function(token, status, xhr, onDone) { + item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; + let exp = dapUtils.dapExtractExpiryFromToken(token) + if (typeof exp == 'number') { + item.expires_at = exp - 10; + } + item.token = token; + localStorage.setItem(DAP_TOKEN, JSON.stringify(item)); + dapUtils.dapLog('Successfully updated and stored token; expires at ' + item.expires_at); + let dapSSID = xhr.getResponseHeader('Akamai-DAP-SS-ID'); + if (dapSSID) { + localStorage.setItem(DAP_SS_ID, JSON.stringify(dapSSID)); + } + let deviceId100 = xhr.getResponseHeader('Akamai-DAP-100'); + if (deviceId100 != null) { + localStorage.setItem('dap_deviceId100', deviceId100); + dapUtils.dapLog('Successfully stored DAP 100 Device ID: ' + deviceId100); + } + if (refreshMembership) { + if (config.segtax == 504) { + dapUtils.dapRefreshEncryptedMembership(config, token, onDone); + } else { + dapUtils.dapRefreshMembership(config, token, onDone); + } + } + }, + function(xhr, status, error, onDone) { + logError('ERROR(' + error + '): failed to retrieve token! ' + status); + onDone() + } + ); + }, + + dapGetMembershipFromLocalStorage: function() { let now = Math.round(Date.now() / 1000.0); // in seconds - let storageName = 'async_dap_membership'; - let maxTtl = 3600; // if the cached membership is older than this, return null let membership = null; - let item = JSON.parse(localStorage.getItem(storageName)); - if (item == null || (now - item.expires_at) > maxTtl) { - item = { - expires_at: now - 1, - said: null, - cohorts: null, - attributes: null - }; - } else { - membership = { - said: item.said, - cohorts: item.cohorts, - attributes: null - }; + let item = JSON.parse(localStorage.getItem(DAP_MEMBERSHIP)); + if (item) { + if (now < item.expires_at) { + membership = { + said: item.said, + cohorts: item.cohorts, + attributes: null + }; + } } + return membership; + }, - // Always refresh the cached membership. + dapRefreshMembership: function(config, token, onDone) { + let now = Math.round(Date.now() / 1000.0); // in seconds + let item = {} let configAsync = {...config}; - dapUtils.dapMembership(configAsync, token, - function(membership, status, xhr) { - item.expires_at = now + maxTtl; + dapUtils.dapMembership(configAsync, token, onDone, + function(membership, status, xhr, onDone) { + item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; + let exp = dapUtils.dapExtractExpiryFromToken(membership.said) + if (typeof exp == 'number') { + item.expires_at = exp - 10; + } item.said = membership.said; item.cohorts = membership.cohorts; - localStorage.setItem(storageName, JSON.stringify(item)); + localStorage.setItem(DAP_MEMBERSHIP, JSON.stringify(item)); dapUtils.dapLog('Successfully updated and stored membership:'); dapUtils.dapLog(item); + + let data = dapUtils.dapGetRtdObj(item, config.segtax) + dapUtils.checkAndAddRealtimeData(data, config.segtax); + onDone(); }, - function(xhr, status, error) { + function(xhr, status, error, onDone) { logError('ERROR(' + error + '): failed to retrieve membership! ' + status); + if (status == 403 && dapRetryTokenize < DAP_MAX_RETRY_TOKENIZE) { + dapRetryTokenize++; + dapUtils.dapRefreshToken(config, true, onDone); + } else { + onDone(); + } } ); + }, - return membership; + dapGetEncryptedMembershipFromLocalStorage: function() { + let now = Math.round(Date.now() / 1000.0); // in seconds + let encMembership = null; + let item = JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)); + if (item) { + if (now < item.expires_at) { + encMembership = { + encryptedSegments: item.encryptedSegments + }; + } + } + return encMembership; + }, + + dapRefreshEncryptedMembership: function(config, token, onDone) { + let now = Math.round(Date.now() / 1000.0); // in seconds + let item = {}; + let configAsync = {...config}; + dapUtils.dapEncryptedMembership(configAsync, token, onDone, + function(encToken, status, xhr, onDone) { + item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; + let exp = dapUtils.dapExtractExpiryFromToken(encToken) + if (typeof exp == 'number') { + item.expires_at = exp - 10; + } + item.encryptedSegments = encToken; + localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(item)); + dapUtils.dapLog('Successfully updated and stored encrypted membership:'); + dapUtils.dapLog(item); + + let encData = dapUtils.dapGetEncryptedRtdObj(item, config.segtax); + dapUtils.checkAndAddRealtimeData(encData, config.segtax); + onDone(); + }, + function(xhr, status, error, onDone) { + logError('ERROR(' + error + '): failed to retrieve encrypted membership! ' + status); + if (status == 403 && dapRetryTokenize < DAP_MAX_RETRY_TOKENIZE) { + dapRetryTokenize++; + dapUtils.dapRefreshToken(config, true, onDone); + } else { + onDone(); + } + } + ); + }, + + /** + * DESCRIPTION + * Extract expiry value from a token + */ + dapExtractExpiryFromToken: function(token) { + let exp = null; + if (token) { + const tokenArray = token.split('..'); + if (tokenArray && tokenArray.length > 0) { + let decode = atob(tokenArray[0]) + let header = JSON.parse(decode.replace(/"/g, '"')); + exp = header.exp; + } + } + return exp }, /** * DESCRIPTION * * Convert a DAP membership response to an OpenRTB2 segment object suitable - * for insertion into user.data.segment or site.data.segment. + * for insertion into user.data.segment or site.data.segment and add it to the rtd obj. */ - dapMembershipToRtbSegment: function(membership, config) { + dapGetRtdObj: function(membership, segtax) { let segment = { name: 'dap.akamai.com', ext: { - 'segtax': config.segtax + 'segtax': segtax }, segment: [] }; @@ -234,7 +558,83 @@ export const dapUtils = { segment.segment.push({ id: i }); } } - return segment; + let data = { + rtd: { + ortb2: { + user: { + data: [ + segment + ] + }, + site: { + ext: { + data: { + dapSAID: membership.said + } + } + } + } + } + }; + return data; + }, + + /** + * DESCRIPTION + * + * Convert a DAP membership response to an OpenRTB2 segment object suitable + * for insertion into user.data.segment or site.data.segment and add it to the rtd obj. + */ + dapGetEncryptedRtdObj: function(encToken, segtax) { + let segment = { + name: 'dap.akamai.com', + ext: { + 'segtax': segtax + }, + segment: [] + }; + if (encToken != null) { + segment.segment.push({ id: encToken.encryptedSegments }); + } + let encData = { + rtd: { + ortb2: { + user: { + data: [ + segment + ] + } + } + } + }; + return encData; + }, + + checkAndAddRealtimeData: function(data, segtax) { + if (data.rtd) { + if (segtax == 504 && dapUtils.checkIfSegmentsAlreadyExist(data.rtd, 504)) { + logMessage('DEBUG(handleInit): rtb Object already added'); + } else { + addRealTimeData(data.rtd); + } + logInfo('DEBUG(getRealTimeData) - 1'); + } + }, + + checkIfSegmentsAlreadyExist: function(rtd, segtax) { + let segmentsExist = false + let ortb2 = config.getConfig('ortb2') || {}; + if (ortb2.user && ortb2.user.data && ortb2.user.data.length > 0) { + for (let i = 0; i < ortb2.user.data.length; i++) { + let element = ortb2.user.data[i] + if (element.ext && element.ext.segtax == segtax) { + segmentsExist = true + logMessage('DEBUG(checkIfSegmentsAlreadyExist): rtb Object already added: ', ortb2.user.data); + break; + } + } + } + return segmentsExist }, dapLog: function(args) { @@ -293,23 +693,23 @@ export const dapUtils = { * function( response, status, xhr } { token = response; }, * function( xhr, status, error ) { ; } // handle error */ - dapTokenize: function(config, identity, onSuccess = null, onError = null) { + dapTokenize: function(config, identity, onDone, onSuccess = null, onError = null) { if (onError == null) { - onError = function(xhr, status, error) {}; + onError = function(xhr, status, error, onDone) {}; } if (config == null || typeof (config) == typeof (undefined)) { - onError(null, 'Invalid config object', 'ClientError'); + onError(null, 'Invalid config object', 'ClientError', onDone); return; } if (typeof (config.domain) != 'string') { - onError(null, 'Invalid config.domain: must be a string', 'ClientError'); + onError(null, 'Invalid config.domain: must be a string', 'ClientError', onDone); return; } if (config.domain.length <= 0) { - onError(null, 'Invalid config.domain: must have non-zero length', 'ClientError'); + onError(null, 'Invalid config.domain: must have non-zero length', 'ClientError', onDone); return; } @@ -318,22 +718,22 @@ export const dapUtils = { } if (typeof (config.api_version) != 'string') { - onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError'); + onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); return; } if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { - onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError'); + onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); return; } if (identity == null || typeof (identity) == typeof (undefined)) { - onError(null, 'Invalid identity object', 'ClientError'); + onError(null, 'Invalid identity object', 'ClientError', onDone); return; } if (!('type' in identity) || typeof (identity.type) != 'string' || identity.type.length <= 0) { - onError(null, "Identity must contain a valid 'type' field", 'ClientError'); + onError(null, "Identity must contain a valid 'type' field", 'ClientError', onDone); return; } @@ -348,6 +748,16 @@ export const dapUtils = { apiParams.attributes = identity.attributes; } + let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); + if (entropyDict.entropy) { + let audioFp = JSON.parse(localStorage.getItem(DAP_AUDIO_FP)); + audioFp = audioFp || 'NA'; + entropyDict.entropy.e17 = audioFp; + apiParams.entropy = entropyDict.entropy; + } else { + logMessage('Entropy not added to Tokenize apiParams.'); + } + let method; let body; let path; @@ -359,10 +769,16 @@ export const dapUtils = { body = JSON.stringify(apiParams); break; default: - onError(null, 'Invalid api_version: ' + config.api_version, 'ClientError'); + onError(null, 'Invalid api_version: ' + config.api_version, 'ClientError', onDone); return; } + let customHeaders = {'Content-Type': 'application/json'}; + let dapSSID = JSON.parse(localStorage.getItem(DAP_SS_ID)); + if (dapSSID) { + customHeaders['Akamai-DAP-SS-ID'] = dapSSID; + } + let url = 'https://' + config.api_hostname + path; let cb = { success: (response, request) => { @@ -373,19 +789,16 @@ export const dapUtils = { token = request.getResponseHeader('Akamai-DAP-Token'); break; } - onSuccess(token, request.status, request); + onSuccess(token, request.status, request, onDone); }, error: (request, error) => { - onError(request, request.statusText, error); + onError(request, request.statusText, error, onDone); } }; ajax(url, cb, body, { method: method, - customHeaders: { - 'Content-Type': 'application/json', - 'Pragma': 'akamai-x-cache-on' - } + customHeaders: customHeaders }); }, @@ -411,7 +824,7 @@ export const dapUtils = { * api_hostname: 'api.dap.akadns.net', * }; * - * // token from dap_x1_tokenize + * // token from dap_tokenize * * dapMembership( config, token, * function( membership, status, xhr ) { @@ -422,13 +835,13 @@ export const dapUtils = { * } ); * */ - dapMembership: function(config, token, onSuccess = null, onError = null) { + dapMembership: function(config, token, onDone, onSuccess = null, onError = null) { if (onError == null) { - onError = function(xhr, status, error) {}; + onError = function(xhr, status, error, onDone) {}; } if (config == null || typeof (config) == typeof (undefined)) { - onError(null, 'Invalid config object', 'ClientError'); + onError(null, 'Invalid config object', 'ClientError', onDone); return; } @@ -437,17 +850,17 @@ export const dapUtils = { } if (typeof (config.api_version) != 'string') { - onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError'); + onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); return; } if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { - onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError'); + onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); return; } if (token == null || typeof (token) != 'string') { - onError(null, 'Invalid token: must be a non-null string', 'ClientError'); + onError(null, 'Invalid token: must be a non-null string', 'ClientError', onDone); return; } let path = '/data-activation/' + @@ -459,10 +872,10 @@ export const dapUtils = { let cb = { success: (response, request) => { - onSuccess(JSON.parse(response), request.status, request); + onSuccess(JSON.parse(response), request.status, request, onDone); }, error: (error, request) => { - onError(request, request.status, error); + onError(request, request.status, error, onDone); } }; @@ -470,5 +883,91 @@ export const dapUtils = { method: 'GET', customHeaders: {} }); + }, + + /** + * SYNOPSIS + * + * dapEncryptedMembership( config, token, onSuccess, onError ); + * + * DESCRIPTION + * + * Return the audience segment membership along with a new Secure Advertising + * ID for this token in encrypted format. + * + * PARAMETERS + * + * config: an array of system configuration parameters + * + * token: the token previously returned from the tokenize API + * + * EXAMPLE + * + * config = { + * api_hostname: 'api.dap.akadns.net', + * }; + * + * // token from dap_tokenize + * + * dapEncryptedMembership( config, token, + * function( membership, status, xhr ) { + * // Run auction with membership.segments and membership.said after decryption + * }, + * function( xhr, status, error ) { + * // error + * } ); + * + */ + dapEncryptedMembership: function(config, token, onDone, onSuccess = null, onError = null) { + if (onError == null) { + onError = function(xhr, status, error, onDone) {}; + } + + if (config == null || typeof (config) == typeof (undefined)) { + onError(null, 'Invalid config object', 'ClientError', onDone); + return; + } + + if (!('api_version' in config) || (typeof (config.api_version) == 'string' && config.api_version.length == 0)) { + config.api_version = 'x1'; + } + + if (typeof (config.api_version) != 'string') { + onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); + return; + } + + if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { + onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); + return; + } + + if (token == null || typeof (token) != 'string') { + onError(null, 'Invalid token: must be a non-null string', 'ClientError', onDone); + return; + } + let path = '/data-activation/' + + config.api_version + + '/token/' + token + + '/membership/encrypt'; + + let url = 'https://' + config.api_hostname + path; + + let cb = { + success: (response, request) => { + let encToken = request.getResponseHeader('Akamai-DAP-Token'); + onSuccess(encToken, request.status, request, onDone); + }, + error: (error, request) => { + onError(request, request.status, error, onDone); + } + }; + ajax(url, cb, undefined, { + method: 'GET', + customHeaders: { + 'Content-Type': 'application/json', + 'Pragma': 'akamai-x-get-extracted-values' + } + }); } } diff --git a/modules/akamaiDapRtdProvider.md b/modules/akamaiDapRtdProvider.md index ade11b88602..435744c264f 100644 --- a/modules/akamaiDapRtdProvider.md +++ b/modules/akamaiDapRtdProvider.md @@ -25,9 +25,8 @@ apiHostname: '', apiVersion: "x1", domain: 'your-domain.com', - identityType: 'email' | 'mobile' | ... | 'dap-signature:1.0.0', - segtax: , - tokenTtl: 5, + identityType: 'email' | 'mobile' | ... | 'dap-signature:1.3.0', + segtax: 504 } } ] diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index b350c2bb529..9a1e718f6c9 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -1,13 +1,14 @@ import {config} from 'src/config.js'; -import {SEGMENTS_STORAGE_KEY, TOKEN_STORAGE_KEY, dapUtils, addRealTimeData, getRealTimeData, akamaiDapRtdSubmodule, storage} from 'modules/akamaiDapRtdProvider.js'; +import { + dapUtils, + getRealTimeData, + akamaiDapRtdSubmodule, + storage, DAP_MAX_RETRY_TOKENIZE, DAP_SS_ID, DAP_TOKEN, DAP_MEMBERSHIP, DAP_ENCRYPTED_MEMBERSHIP, +} from 'modules/akamaiDapRtdProvider.js'; import {server} from 'test/mocks/xhr.js'; -import logMessage from 'src/utils.js' const responseHeader = {'Content-Type': 'application/json'}; describe('akamaiDapRtdProvider', function() { - let getDataFromLocalStorageStub; - let getDapTokenStub; - const testReqBidsConfigObj = { adUnits: [ { @@ -18,7 +19,9 @@ describe('akamaiDapRtdProvider', function() { const onDone = function() { return true }; - const onSuccess = function() { return ('request', 200, 'success') }; + const sampleIdentity = { + type: 'dap-signature:1.0.0' + }; const cmoduleConfig = { 'name': 'dap', @@ -28,8 +31,19 @@ describe('akamaiDapRtdProvider', function() { 'apiVersion': 'x1', 'domain': 'prebid.org', 'identityType': 'dap-signature:1.0.0', - 'segtax': 503, - 'tokenTtl': 5 + 'segtax': 503 + } + } + + const emoduleConfig = { + 'name': 'dap', + 'waitForIt': true, + 'params': { + 'apiHostname': 'prebid.dap.akadns.net', + 'apiVersion': 'x1', + 'domain': 'prebid.org', + 'identityType': 'dap-signature:1.0.0', + 'segtax': 504 } } @@ -37,81 +51,119 @@ describe('akamaiDapRtdProvider', function() { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 'x1', 'domain': 'prebid.org', - 'segtax': 503 + 'segtax': 503, + 'identity': sampleIdentity } - const sampleIdentity = { - type: 'dap-signature:1.0.0' + + const esampleConfig = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 'x1', + 'domain': 'prebid.org', + 'segtax': 504, + 'identity': sampleIdentity + } + let cacheExpiry = Math.round(Date.now() / 1000.0) + 300; // in seconds + const sampleCachedToken = {'expires_at': cacheExpiry, 'token': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..6buzBd2BjtgoyaNbHN8YnQ.l38avCfm3sYNy798-ETYOugz0cOx1cCkjACkAhYszxzrZ0sUJ0AiF-NdDXVTiTyp2Ih3vCWKzS0rKJ8lbS1zhyEVWVu91QwtwseM2fBbwA5ggAgBEo5wV-IXqDLPxVnxsPF0D3hP6cNCiH9Q2c-vULfsLhMhG5zvvZDPBbn4hUY5fKB8LoCBTF9rbuuWGYK1nramnb4AlS5UK82wBsHQea1Ou_Kp5wWCMNZ6TZk5qKIuRBfPIAhQblWvHECaHXkg1wyoM9VASs_yNhne7RR-qkwzbFiPFiMJibNOt9hF3_vPDJO5-06ZBjRTP1BllYGWxI-uQX6InzN18Wtun2WHqg.63sH0SNlIRcsK57v0pMujfB_nhU8Y5CuQbsHqH5MGoM'}; + const cachedEncryptedMembership = {'expires_at': cacheExpiry, 'encryptedSegments': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQifQ..IvnIUQDqWBVYIS0gbcE9bw.Z4NZGvtogWaWlGH4e-GdYKe_PUc15M2x3Bj85rMWsN1A17mIxQIMOfg2hsQ2tgieLu5LggWPmsFu1Wbph6P0k3kOu1dVReoIhOHzxw50rP0DLHKaEZ5mLMJ7Lcosvwh4miIfFuCHlsX7J0sFgOTAp0zGo1S_UsHLtev1JflhjoSB0AoX95ALbAnyctirPuLJM8gZ1vXTiZ01jpvucGyR1lM4cWjPOeD8jPtgwaPGgSRZXE-3X2Cqy7z4Giam5Uqu74LPWTBuKtUQTGyAXA5QJoP7xwTbsU4O1f69lu3fWNqC92GijeTH1A4Zd_C-WXxWuQlDEURjlkWQoaqTHka2OqlnwukEQIf_v0r5KQQX64CTLhEUH91jeD0-E9ClcIP7pwOLxxqiKoaBmx8Mrnm_6Agj5DtTA1rusy3AL63sI_rsUxrmLrVt0Wft4aCfRkW8QpQxu8clFdOmce0NNCGeBCyCPVw9d9izrILlXJ6rItU2cpFrcbz8uw2otamF5eOFCOY3IzHedWVNNuKHFIUVC_xYSlsYvQ8f2QIP1eiMbmukcuPzmTzjw1h1_7IKaj-jJkXrnrY-TdDgX_4-_Z3rmbpXK2yTR7dBrsg-ubqFbgbKic1b4zlQEO_LbBlgPl3DYdWEuJ8CY2NUt1GfpATQGsufS2FTY1YGw_gkPe3q04l_cgLafDoxHvHh_t_0ZgPjciW82gThB_kN4RP7Mc3krVcXl_P6N1VbV07xyx0hCyVsrrxbLslI8q9wYDiLGci7mNmByM5j7SXV9jPwwPkHtn0HfMJlw2PFbIDPjgG3h7sOyLcBIJTTvuUIgpHPIkRWLIl_4FlIucXbJ7orW2nt5BWleBVHgumzGcnl9ZNcZb3W-dsdYPSOmuj0CY28MRTP2oJ1rzLInbDDpIRffJBtR7SS4nYyy7Vi09PtBigod5YNz1Q0WDSJxr8zeH_aKFaXInw7Bfo_U0IAcLiRgcT0ogsMLeQRjRFy27mr4XNJv3NtHhbdjDAwF2aClCktXyXbQaVdsPH2W71v6m2Q9rB5GQWOktw2s5f-4N1-_EBPGq6TgjF-aJZP22MJVwp1pimT50DfOzoeEqDwi862NNwNNoHmcObH0ZfwAXlhRxsgupNBe20-MNNABj2Phlfv4DUrtQbMdfCnNiypzNCmoTb7G7c_o5_JUwoV_GVkwUtvmi_IUm05P4GeMASSUw8zDKVRAj9h31C2cabM8RjMHGhkbCWpUP2pcz9zlJ7Y76Dh3RLnctfTw7DG9U4w4UlaxNZOgLUiSrGwfyapuSiuGUpuOJkBBLiHmEqAGI5C8oJpcVRccNlHxJAYowgXyFopD5Fr-FkXmv8KMkS0h5C9F6KihmDt5sqDD0qnjM0hHJgq01l7wjVnhEmPpyD-6auFQ-xDnbh1uBOJ_0gCVbRad--FSa5p-dXenggegRxOvZXJ0iAtM6Fal5Og-RCjexIHa9WhVbXhQBJpkSTWwAajZJ64eQ.yih49XB51wE-Xob7COT9OYqBrzBmIMVCQbLFx2UdzkI'}; + const cachedMembership = {'expires_at': cacheExpiry, 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..QwvU5h0NVJYaJbs5EqWCKA.XNaJHSlnsH8P-yBIr3gIEqavLONWDIFyj7QCHFwJVkwXH_EYkxrk0_26b0uMPzfJp5URnqxKZusMH9DzEJsmj8EMrKQv1y3IYYMsW5_0BdP5bcAWfG6fzOqtMOwLiYRkYiQOqn1ZVGzhovheHWEmNr2_oCY0LvAr3iN1eG_K-l-bBKvBWnwvuuGKquUfCqO8NMMq6wtkecEXM9blqFRZ7oNYmW2aIG7qcHUsrUW7HMr9Ev2Ik0sIeEUsOYrgf_X_VA64RgKSTRugS9FupMv1p54JkHokwduF9pOFmW8QLQi8itFogKGbbgvOTNnmahxQUX5FcrjjYLqHwKqC8htLdlHnO5LWU9l4A7vLXrRurvoSnh0cAJy0GsdoyEwTqR9bwVFHoPquxlJjQ4buEd7PIxpBj9Qg9oOPH3b2upbMTu5CQ9oj526eXPhP5G54nwGklm2AZ3Vggd7jCQJn45Jjiq0iIfsXAtpqS2BssCLBN8WhmUTnStK8m5sux6WUBdrpDESQjPj-EEHVS-DB5rA7icRUh6EzRxzen2rndvHvnwVhSG_l6cwPYuJ0HE0KBmYHOoqNpKwzoGiKFHrf4ReA06iWB3V2TEGJucGujhtQ9_18WwHCeJ1XtQiiO1eqa3tp5MwAbFXawVFl3FFOBgadrPyvGmkmUJ6FCLU2MSwHiYZmANMnJsokFX_6DwoAgO3U_QnvEHIVSvefc7ReeJ8fBDdmrH3LtuLrUpXsvLvEIMQdWQ_SXhjKIi7tOODR8CfrhUcdIjsp3PZs1DpuOcDB6YJKbGnKZTluLUJi3TyHgyi-DHXdTm-jSE5i_DYJGW-t2Gf23FoQhexv4q7gdrfsKfcRJNrZLp6Gd6jl4zHhUtY.nprKBsy9taQBk6dCPbA7BFF0CiGhQOEF_MazZ2bedqk', 'cohorts': ['9', '11', '13']}; + const rtdUserObj = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const encRtdUserObj = { + name: 'www.dataprovider3.com', + ext: { + segtax: 504, + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [] + }; + + const cachedRtd = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj] + } + } + } + }; + + let membership = { + said: cachedMembership.said, + cohorts: cachedMembership.cohorts, + attributes: null + }; + let encMembership = { + encryptedSegments: cachedEncryptedMembership.encryptedSegments + }; + encRtdUserObj.segment.push({ id: encMembership.encryptedSegments }); + const cachedEncRtd = { + rtd: { + ortb2: { + user: { + data: [encRtdUserObj] + } + } + } }; beforeEach(function() { config.resetConfig(); - getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') + localStorage.removeItem(DAP_TOKEN); + localStorage.removeItem(DAP_MEMBERSHIP); + localStorage.removeItem(DAP_ENCRYPTED_MEMBERSHIP); + localStorage.removeItem(DAP_SS_ID); }); afterEach(function () { - getDataFromLocalStorageStub.restore(); }); describe('akamaiDapRtdSubmodule', function() { it('successfully instantiates', function () { - expect(akamaiDapRtdSubmodule.init()).to.equal(true); + expect(akamaiDapRtdSubmodule.init()).to.equal(true); }); }); describe('Get Real-Time Data', function() { it('gets rtd from local storage cache', function() { - const rtdConfig = { - params: { - segmentCache: true - } - }; - const bidConfig = {}; - - const rtdUserObj1 = { - name: 'www.dataprovider3.com', - ext: { - taxonomyname: 'iab_audience_taxonomy' - }, - segment: [ - { - id: '1918' - }, - { - id: '1939' - } - ] - }; - - const cachedRtd = { - rtd: { - ortb2: { - user: { - data: [rtdUserObj1] - } - } - } - }; - - getDataFromLocalStorageStub.withArgs(SEGMENTS_STORAGE_KEY).returns(JSON.stringify(cachedRtd)); + localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + let dapGetMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership) + let dapGetRtdObjStub = sinon.stub(dapUtils, 'dapGetRtdObj').returns(cachedRtd) + let dapGetEncryptedMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership) + let dapGetEncryptedRtdObjStub = sinon.stub(dapUtils, 'dapGetEncryptedRtdObj').returns(cachedEncRtd) expect(config.getConfig().ortb2).to.be.undefined; - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); - }); - - it('should initalise and return with config', function () { - expect(getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig)).to.equal(undefined) + getRealTimeData(bidConfig, () => {}, emoduleConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([encRtdUserObj]); + getRealTimeData(bidConfig, () => {}, cmoduleConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj]); + dapGetRtdObjStub.restore() + dapGetMembershipFromLocalStorageStub.restore() + dapGetEncryptedRtdObjStub.restore() + dapGetEncryptedMembershipFromLocalStorageStub.restore() }); }); describe('dapTokenize', function () { it('dapTokenize error callback', function () { let configAsync = JSON.parse(JSON.stringify(sampleConfig)); - let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, - function(token, status, xhr) { + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { }, - function(xhr, status, error) { + function(xhr, status, error, onDone) { } ); let request = server.requests[0]; @@ -121,10 +173,10 @@ describe('akamaiDapRtdProvider', function() { it('dapTokenize success callback', function () { let configAsync = JSON.parse(JSON.stringify(sampleConfig)); - let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, - function(token, status, xhr) { + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { }, - function(xhr, status, error) { + function(xhr, status, error, onDone) { } ); let request = server.requests[0]; @@ -135,40 +187,54 @@ describe('akamaiDapRtdProvider', function() { describe('dapTokenize and dapMembership incorrect params', function () { it('Onerror and config are null', function () { - expect(dapUtils.dapTokenize(null, 'identity', null, null)).to.be.equal(undefined); - expect(dapUtils.dapMembership(null, 'identity', null, null)).to.be.equal(undefined); + expect(dapUtils.dapTokenize(null, 'identity', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapMembership(null, 'identity', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapEncryptedMembership(null, 'identity', onDone, null, null)).to.be.equal(undefined); const config = { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 1, 'domain': '', 'segtax': 503 }; + const encConfig = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 1, + 'domain': '', + 'segtax': 504 + }; let identity = { type: 'dap-signature:1.0.0' }; - expect(dapUtils.dapTokenize(config, identity, null, null)).to.be.equal(undefined); - expect(dapUtils.dapMembership(config, 'token', null, null)).to.be.equal(undefined); + expect(dapUtils.dapTokenize(config, identity, onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapMembership(config, 'token', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapEncryptedMembership(encConfig, 'token', onDone, null, null)).to.be.equal(undefined); }); + }); - it('dapGetToken success', function () { - let dapTokenizeStub = sinon.stub(dapUtils, 'dapTokenize').returns(onSuccess); - expect(dapUtils.dapGetToken(sampleConfig, 'token', - function(token, status, xhr) { - }, - function(xhr, status, error) { - } - )).to.be.equal(null); - dapTokenizeStub.restore(); + describe('Getting dapTokenize, dapMembership and dapEncryptedMembership from localstorage', function () { + it('dapGetTokenFromLocalStorage success', function () { + localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + expect(dapUtils.dapGetTokenFromLocalStorage(60)).to.be.equal(sampleCachedToken.token); + }); + + it('dapGetMembershipFromLocalStorage success', function () { + localStorage.setItem(DAP_MEMBERSHIP, JSON.stringify(cachedMembership)); + expect(JSON.stringify(dapUtils.dapGetMembershipFromLocalStorage())).to.be.equal(JSON.stringify(membership)); + }); + + it('dapGetEncryptedMembershipFromLocalStorage success', function () { + localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)); + expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.be.equal(JSON.stringify(encMembership)); }); }); describe('dapMembership', function () { it('dapMembership success callback', function () { let configAsync = JSON.parse(JSON.stringify(sampleConfig)); - let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', - function(token, status, xhr) { + let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { }, - function(xhr, status, error) { + function(xhr, status, error, onDone) { } ); let request = server.requests[0]; @@ -178,10 +244,38 @@ describe('akamaiDapRtdProvider', function() { it('dapMembership error callback', function () { let configAsync = JSON.parse(JSON.stringify(sampleConfig)); - let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', - function(token, status, xhr) { + let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { }, - function(xhr, status, error) { + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(400, responseHeader, JSON.stringify('error')); + expect(submoduleCallback).to.equal(undefined); + }); + }); + + describe('dapEncMembership', function () { + it('dapEncMembership success callback', function () { + let configAsync = JSON.parse(JSON.stringify(esampleConfig)); + let submoduleCallback = dapUtils.dapEncryptedMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify('success')); + expect(submoduleCallback).to.equal(undefined); + }); + + it('dapEncMembership error callback', function () { + let configAsync = JSON.parse(JSON.stringify(esampleConfig)); + let submoduleCallback = dapUtils.dapEncryptedMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { } ); let request = server.requests[0]; @@ -192,55 +286,214 @@ describe('akamaiDapRtdProvider', function() { describe('dapMembership', function () { it('should invoke the getDapToken and getDapMembership', function () { - let config = { - api_hostname: cmoduleConfig.params.apiHostname, - api_version: cmoduleConfig.params.apiVersion, - domain: cmoduleConfig.params.domain, - segtax: cmoduleConfig.params.segtax - }; - let identity = { - type: cmoduleConfig.params.identityType - }; - let membership = { said: 'item.said1', cohorts: 'item.cohorts', attributes: null }; - let getDapTokenStub = sinon.stub(dapUtils, 'dapGetToken').returns('token3'); - let getDapMembershipStub = sinon.stub(dapUtils, 'dapGetMembership').returns(membership); - let dapTokenizeStub = sinon.stub(dapUtils, 'dapTokenize').returns('response', 200, 'request'); + let getDapMembershipStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership); getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig); - expect(getDapTokenStub.calledOnce).to.be.equal(true); expect(getDapMembershipStub.calledOnce).to.be.equal(true); - getDapTokenStub.restore(); getDapMembershipStub.restore(); - dapTokenizeStub.restore(); }); }); - describe('dapMembershipToRtbSegment', function () { - it('dapMembershipToRtbSegment', function () { - let membership1 = { - said: 'item.said1', - cohorts: 'item.cohorts', - attributes: null + describe('dapEncMembership test', function () { + it('should invoke the getDapToken and getEncDapMembership', function () { + let encMembership = { + encryptedSegments: 'enc.seg', }; + + let getDapEncMembershipStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership); + getRealTimeData(testReqBidsConfigObj, onDone, emoduleConfig); + expect(getDapEncMembershipStub.calledOnce).to.be.equal(true); + getDapEncMembershipStub.restore(); + }); + }); + + describe('dapGetRtdObj test', function () { + it('dapGetRtdObj', function () { const config = { apiHostname: 'prebid.dap.akadns.net', apiVersion: 'x1', domain: 'prebid.org', - tokenTtl: 5, segtax: 503 }; - let identity = { - type: 'dap-signature:1.0.0' - }; - - expect(dapUtils.dapGetMembership(config, 'token')).to.equal(null) + expect(dapUtils.dapRefreshMembership(config, 'token', onDone)).to.equal(undefined) const membership = {cohorts: ['1', '5', '7']} - expect(dapUtils.dapMembershipToRtbSegment(membership, config)).to.not.equal(undefined); + expect(dapUtils.dapGetRtdObj(membership, config.segtax)).to.not.equal(undefined); + }); + }); + + describe('checkAndAddRealtimeData test', function () { + it('add realtime data for segtax 503 and 504', function () { + dapUtils.checkAndAddRealtimeData(cachedEncRtd, 504); + dapUtils.checkAndAddRealtimeData(cachedEncRtd, 504); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([encRtdUserObj]); + dapUtils.checkAndAddRealtimeData(cachedRtd, 503); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj]); + }); + }); + + describe('dapExtractExpiryFromToken test', function () { + it('test dapExtractExpiryFromToken function', function () { + let tokenWithoutExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..6buzBd2BjtgoyaNbHN8YnQ.l38avCfm3sYNy798-ETYOugz0cOx1cCkjACkAhYszxzrZ0sUJ0AiF-NdDXVTiTyp2Ih3vCWKzS0rKJ8lbS1zhyEVWVu91QwtwseM2fBbwA5ggAgBEo5wV-IXqDLPxVnxsPF0D3hP6cNCiH9Q2c-vULfsLhMhG5zvvZDPBbn4hUY5fKB8LoCBTF9rbuuWGYK1nramnb4AlS5UK82wBsHQea1Ou_Kp5wWCMNZ6TZk5qKIuRBfPIAhQblWvHECaHXkg1wyoM9VASs_yNhne7RR-qkwzbFiPFiMJibNOt9hF3_vPDJO5-06ZBjRTP1BllYGWxI-uQX6InzN18Wtun2WHqg.63sH0SNlIRcsK57v0pMujfB_nhU8Y5CuQbsHqH5MGoM' + expect(dapUtils.dapExtractExpiryFromToken(tokenWithoutExpiry)).to.equal(undefined); + let tokenWithExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQzODMwMzY5fQ..hTbcSQgmmO0HUJJrQ5fRHw.7zjrQXNNVkb-GD0ZhIVhEPcWbyaDBilHTWv-bp1lFZ9mdkSC0QbcAvUbYteiTD7ya23GUwcL2WOW8WgRSHaWHOJe0B5NDqfdUGTzElWfu7fFodRxRgGmwG8Rq5xxteFKLLGHLf1mFYRJKDtjtgajGNUKIDfn9AEt-c5Qz4KU8VolG_KzrLROx-f6Z7MnoPTcwRCj0WjXD6j2D6RAZ80-mKTNIsMIELdj6xiabHcjDJ1WzwtwCZSE2y2nMs451pSYp8W-bFPfZmDDwrkjN4s9ASLlIXcXgxK-H0GsiEbckQOZ49zsIKyFtasBvZW8339rrXi1js-aBh99M7aS5w9DmXPpUDmppSPpwkeTfKiqF0cQiAUq8tpeEQrGDJuw3Qt2.XI8h9Xw-VZj_NOmKtV19wLM63S4snos7rzkoHf9FXCw' + expect(dapUtils.dapExtractExpiryFromToken(tokenWithExpiry)).to.equal(1643830369); + }); + }); + + describe('dapRefreshToken test', function () { + it('test dapRefreshToken success response', function () { + dapUtils.dapRefreshToken(sampleConfig, true, onDone) + let request = server.requests[0]; + responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; + request.respond(200, responseHeader, JSON.stringify(sampleCachedToken.token)); + expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).token).to.be.equal(sampleCachedToken.token); + }); + + it('test dapRefreshToken success response with deviceid 100', function () { + dapUtils.dapRefreshToken(esampleConfig, true, onDone) + let request = server.requests[0]; + responseHeader['Akamai-DAP-100'] = sampleCachedToken.token; + request.respond(200, responseHeader, ''); + expect(localStorage.getItem('dap_deviceId100')).to.be.equal(sampleCachedToken.token); + }); + + it('test dapRefreshToken success response with exp claim', function () { + dapUtils.dapRefreshToken(sampleConfig, true, onDone) + let request = server.requests[0]; + let tokenWithExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQzODMwMzY5fQ..hTbcSQgmmO0HUJJrQ5fRHw.7zjrQXNNVkb-GD0ZhIVhEPcWbyaDBilHTWv-bp1lFZ9mdkSC0QbcAvUbYteiTD7ya23GUwcL2WOW8WgRSHaWHOJe0B5NDqfdUGTzElWfu7fFodRxRgGmwG8Rq5xxteFKLLGHLf1mFYRJKDtjtgajGNUKIDfn9AEt-c5Qz4KU8VolG_KzrLROx-f6Z7MnoPTcwRCj0WjXD6j2D6RAZ80-mKTNIsMIELdj6xiabHcjDJ1WzwtwCZSE2y2nMs451pSYp8W-bFPfZmDDwrkjN4s9ASLlIXcXgxK-H0GsiEbckQOZ49zsIKyFtasBvZW8339rrXi1js-aBh99M7aS5w9DmXPpUDmppSPpwkeTfKiqF0cQiAUq8tpeEQrGDJuw3Qt2.XI8h9Xw-VZj_NOmKtV19wLM63S4snos7rzkoHf9FXCw' + responseHeader['Akamai-DAP-Token'] = tokenWithExpiry; + request.respond(200, responseHeader, JSON.stringify(tokenWithExpiry)); + expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).expires_at).to.be.equal(1643830359); + }); + + it('test dapRefreshToken error response', function () { + localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + dapUtils.dapRefreshToken(sampleConfig, false, onDone) + let request = server.requests[0]; + request.respond(400, responseHeader, 'error'); + expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).expires_at).to.be.equal(cacheExpiry);// Since the expiry is same, the token is not updated in the cache + }); + }); + + describe('dapRefreshMembership test', function () { + it('test dapRefreshMembership success response', function () { + let membership = {'cohorts': ['9', '11', '13'], 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..17wnrhz6FbWx0Cf6LXpm1A.m9PKVCradk3CZokNKzVHzE06TOqiXYeijgxTQUiQy5Syx-yicnO8DyYX6zQ6rgPcNgUNRt4R4XE5MXuK0laUVQJr9yc9g3vUfQfw69OMYGW_vRlLMPzoNOhF2c4gSyfkRrLr7C0qgALmZO1D11sPflaCTNmO7pmZtRaCOB5buHoWcQhp1bUSJ09DNDb31dX3llimPwjNGSrUhyq_EZl4HopnnjxbM4qVNMY2G_43C_idlVOvbFoTxcDRATd-6MplJoIOIHQLDZEetpIOVcbEYN9gQ_ndBISITwuu5YEgs5C_WPHA25nm6e4BT5R-tawSA8yPyQAupqE8gk4ZWq_2-T0cqyTstIHrMQnZ_vysYN7h6bkzE-KeZRk7GMtySN87_fiu904hLD9QentGegamX6UAbVqQh7Htj7SnMHXkEenjxXAM5mRqQvNCTlw8k-9-VPXs-vTcKLYP8VFf8gMOmuYykgWac1gX-svyAg-24mo8cUbqcsj9relx4Qj5HiXUVyDMBZxK-mHZi-Xz6uv9GlggcsjE13DSszar-j2OetigpdibnJIxRZ-4ew3-vlvZ0Dul3j0LjeWURVBWYWfMjuZ193G7lwR3ohh_NzlNfwOPBK_SYurdAnLh7jJgTW-lVLjH2Dipmi9JwX9s03IQq9opexAn7hlM9oBI6x5asByH8JF8WwZ5GhzDjpDwpSmHPQNGFRSyrx_Sh2CPWNK6C1NJmLkyqAtJ5iw0_al7vPDQyZrKXaLTjBCUnbpJhUZ8dUKtWLzGPjzFXp10muoDIutd1NfyKxk1aWGhx5aerYuLdywv6cT_M8RZTi8924NGj5VA30V5OvEwLLyX93eDhntXZSCbkPHpAfiRZNGXrPY.GhCbWGQz11mIRD4uPKmoAuFXDH7hGnils54zg7N7-TU'} + dapUtils.dapRefreshMembership(sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(membership)); + let rtdObj = dapUtils.dapGetRtdObj(membership, 503) + expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + }); + + it('test dapRefreshMembership success response with exp claim', function () { + let membership = {'cohorts': ['9', '11', '13'], 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQ3OTcxNTU4fQ..ptdM5WO-62ypXlKxFXD4FQ.waEo9MHS2NYQCi-zh_p6HgT9BdqGyQbBq4GfGLfsay4nRBgICsTS-VkV6e7xx5U1T8BgpKkRJIZBwTOY5Pkxk9FpK5nnffDSEljRrp1LXLCkNP4qwrlqHInFbZsonNWW4_mW-7aUPlTwIsTbfjTuyHdXHeQa1ALrwFFFWE7QUmPNd2RsHjDwUsxlJPEb5TnHn5W0Mgo_PQZaxvhJInMbxPgtJLoqnJvOqCBEoQY7au7ALZL_nWK8XIwPMF19J7Z3cBg9vQInhr_E3rMdQcAFHEzYfgoNcIYCCR0t1UOqUE3HNtX-E64kZAYKWdlsBb9eW5Gj9hHYyPNL_4Hntjg5eLXGpsocMg0An-qQKGC6hkrxKzeM-GrjpvSaQLNs4iqDpHUtzA02LW_vkLkMNRUiyXVJ3FUZwfyq6uHSRKWZ6UFdAfL0rfJ8q8x8Ll-qJO2Jfyvidlsi9FIs7x1WJrvDCKepfAQM1UXRTonrQljFBAk83PcL2bmWuJDgJZ0lWS4VnZbIf6A7fDourmkDxdVRptvQq5nSjtzCA6whRw0-wGz8ehNJsaJw9H_nG9k4lRKs7A5Lqsyy7TVFrAPjnA_Q1a2H6xF2ULxrtIqoNqdX7k9RjowEZSQlZgZUOAmI4wzjckdcSyC_pUlYBMcBwmlld34mmOJe9EBHAxjdci7Q_9lvj1HTcwGDcQITXnkW9Ux5Jkt9Naw-IGGrnEIADaT2guUAto8W_Gb05TmwHSd6DCmh4zepQCbqeVe6AvPILtVkTgsTTo27Q-NvS7h-XtthJy8425j5kqwxxpZFJ0l0ytc6DUyNCLJXuxi0JFU6-LoSXcROEMVrHa_Achufr9vHIELwacSAIHuwseEvg_OOu1c1WYEwZH8ynBLSjqzy8AnDj24hYgA0YanPAvDqacrYrTUFqURbHmvcQqLBTcYa_gs7uDx4a1EjtP_NvHRlvCgGAaASrjGMhTX8oJxlTqahhQ.pXm-7KqnNK8sbyyczwkVYhcjgiwkpO8LjBBVw4lcyZE'} + dapUtils.dapRefreshMembership(sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(membership)); + let rtdObj = dapUtils.dapGetRtdObj(membership, 503) + expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(localStorage.getItem(DAP_MEMBERSHIP)).expires_at).to.be.equal(1647971548); + }); + + it('test dapRefreshMembership 400 error response', function () { + dapUtils.dapRefreshMembership(sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(400, responseHeader, 'error'); + expect(config.getConfig().ortb2).to.be.equal(undefined); + }); + + it('test dapRefreshMembership 403 error response', function () { + dapUtils.dapRefreshMembership(sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(403, responseHeader, 'error'); + let requestTokenize = server.requests[1] + requestTokenize.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 1); + }); + }); + + describe('dapRefreshEncryptedMembership test', function () { + it('test dapRefreshEncryptedMembership success response', function () { + let expiry = Math.round(Date.now() / 1000.0) + 3600; // in seconds + let encMembership = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQifQ..f8_At4OqeQXyQcSwThOJ_w.69ImVQ3bEZ6QP7ROCRpAJjNcKY49SEPYR6qTp_8l7L8kQdPbpi4wmuOzt78j7iBrX64k2wltzmQFjDmVKSxDhrEguxpgx6t-L1tT8ZA0UosMWpVsgmKEZxOn2e9ES3jw8RNCS4WSWocSPQX33xSb51evXjm9E1s0tGoLnwXl0GsUvzRsSU86wQG6RZnAQTi7s-r-M2TKibdDjUqgIt62vJ-aBZ7RWw91MINgOdmDNs1bFfbBX5Cy1kd4-kjvRDz_aJ6zHX4sK_7EmQhGEY3tW-A3_l2I88mw-RSJaPkb_IWg0QpVwXDaE2F2g8NpY1PzCRvG_NIE8r28eK5q44OMVitykHmKmBXGDj7z2JVgoXkfo5u0I-dypZARn4GP_7niK932avB-9JD7Mz3TrlU4GZ7IpYfJ91PMsRhrs5xNPQwLZbpuhF76A7Dp7iss71UjkGCiPTU6udfRb4foyf_7xEF66m1eQVcVaMdxEbMuu9GBfdr-d04TbtJhPfUV8JfxTenvRYoi13n0j5kH0M5OgaSQD9kQ3Mrd9u-Cms-BGtT0vf-N8AaFZY_wn0Y4rkpv5HEaH7z3iT4RCHINWrXb_D0WtjLTKQi2YmF8zMlzUOewNJGwZRwbRwxc7JoDIKEc5RZkJYevfJXOEEOPGXZ7AGZxOEsJawPqFqd_nOUosCZS4akHhcDPcVowoecVAV0hhhoS6JEY66PhPp1snbt6yqA-fQhch7z8Y-DZT3Scibvffww3Scg_KFANWp0KeEvHG0vyv9R2F4o66viSS8y21MDnM7Yjk8C-j7aNMldUQbjN_7Yq1nkfe0jiBX_hsINBRPgJHUY4zCaXuyXs-JZZfU92nwG0RT3A_3RP2rpY8-fXp9d3C2QJjEpnmHvTMsuAZCQSBe5DVrJwN_UKedxcJEoOt0wLz6MaCMyYZPd8tnQeqYK1cd3RgQDXtzKC0HDw1En489DqJXEst4eSSkaaW1lImLeaF8XCOaIqPqoyGk4_6KVLw5Q7OnpczuXqYKMd9UTMovGeuTuo1k0ddfEqTq9QwxkwZL51AiDRnwTCAeYBU1krV8FCJQx-mH_WPB5ftZj-o_3pbvANeRk27QBVmjcS-tgDllJkWBxX-4axRXzLw8pUUUZUT_NOL0OiqUCWVm0qMBEpgRQ57Se42-hkLMTzLhhGJOnVcaXU1j4ep-N7faNvbgREBjf_LgzvaWS90a2NJ9bB_J9FyXelhCN_AMLfdOS3fHkeWlZ0u0PMbn5DxXRMe0l9jB-2VJZhcPQRlWoYyoCO3l4F5ZmuQP5Xh9CU4tvSWih6jlwMDgdVWuTpdfPD5bx8ccog3JDq87enx-QtPzLU3gMgouNARJGgNwKS_GJSE1uPrt2oiqgZ3Z0u_I5MKvPdQPV3o-4rsaE730eB4OwAOF-mkGWpzy8Pbl-Qe5PR9mHBhuyJgZ-WDSCHl5yvet2kfO9mPXZlqBQ26fzTcUYH94MULAZn36og6w.3iKGv-Le-AvRmi26W1v6ibRLGbwKbCR92vs-a9t55hw'; + dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + responseHeader['Akamai-DAP-Token'] = encMembership; + request.respond(200, responseHeader, encMembership); + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) + expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(expiry); + }); + + it('test dapRefreshEncryptedMembership success response with exp claim', function () { + let encMembership = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQiLCJleHAiOjE2NDM4MzA2NDB9..inYoxwht_aqTIWqGhEm_Gw.wDcCUOCwtqgnNUouaD723gKfm7X7bgkHgtiX4mr07P3tWk25PUQunmwTLhWBB5CYzzGIfIvveG_u4glNRLi_eRSQV4ihKKk1AN-BSSJ3d0CLAdY9I1WG5vX1VmopXyKnV90bl9SLNqnhg4Vxe6YU4ogTYxsKHuIN1EeIH4hpl-HbCQWQ1DQt4mB-MQF8V9AWTfU0D7sFMSK8f9qj6NGmf1__oHdHUlws0t5V2UAn_dhJexsuREK_gh65pczCuly5eEcziZ82LeP-nOhKWSRHB_tS_mKXrRU6_At_EVDgtfA3PSBJ6eQylCii6bTL42vZzz4jZhJv_3eLfRdKqpVT5CWNBzcDoQ2VcQgKgIBtPJ45KFfAYTQ6kdl21QMSjqtu8GTsv1lEZtrqHY6zRiG8_Mu28-PmjEw4LDdZmBDOeroue_MJD6wuE_jlE7J2iVdo8CkVnoRgzFwNbKBo7CK4z0WahV9rhuOm0LKAN5H0jF_gj696U-3fVTDTIb8ndNKNI2_xAhvWs00BFGtUtWgr8QGDGRTDCNGsDgnb_Vva9xCqVOyAE9O3Fq1QYl-tMA-KkBt3zzvmFFpOxpOyH-lUubKLKlsrxKc3GSyVEQ9DDLhrXXJgR5H5BSE4tjlK7p3ODF5qz0FHtIj7oDcgLazFO7z2MuFy2LjJmd3hKl6ujcfYEDiQ4D3pMIo7oiU33aFBD1YpzI4-WzNfJlUt1FoK0-DAXpbbV95s8p08GOD4q81rPw5hRADKJEr0QzrbDwplTWCzT2fKXMg_dIIc5AGqGKnVRUS6UyF1DnHpudNIJWxyWZjWIEw_QNjU0cDFmyPSyKxNrnfq9w8WE2bfbS5KTicxei5QHnC-cnL7Nh7IXp7WOW6R1YHbNPT7Ad4OhnlV-jjrXwkSv4wMAbfwAWoSCchGh7uvENNAeJymuponlJbOgw_GcYM73hMs8Z8W9qxRfbyF4WX5fDKXg61mMlaieHkc0EnoC5q7uKyXuZUehHZ76JLDFmewslLkQq5SkVCttzJePBnY1ouPEHw5ZTzUnG5f01QQOVcjIN-AqXNDbG5IOwq0heyS6vVfq7lZKJdLDVQ21qRjazGPaqYwLzugkWkzCOzPTgyFdbXzgjfmJwylHSOM5Jpnul84GzxEQF-1mHP2A8wtIT-M7_iX24It2wwWvc8qLA6GEqruWCtNyoug8CXo44mKdSSCGeEZHtfMbzXdLIBHCy2jSHz5i8S7DU_R7rE_5Ssrb81CqIYbgsAQBHtOYoyvzduTOruWcci4De0QcULloqImIEHUuIe2lnYO889_LIx5p7nE3UlSvLBo0sPexavFUtHqI6jdG6ye9tdseUEoNBDXW0aWD4D-KXX1JLtAgToPVUtEaXCJI7QavwO9ZG6UZM6jbfuJ5co0fvUXp6qYrFxPQo2dYHkar0nT6s1Zg5l2g8yWlLUJrHdHAzAw_NScUp71OpM4TmNsLnYaPVPcOxMvtJXTanbNWr0VKc8gy9q3k_1XxAnQwiduNs7f5bA-6qCVpayHv5dE7mUhFEwyh1_w95jEaURsQF_hnnd2OqRkADfiok4ZiPU2b38kFW1LXjpI39XXES3JU0e08Rq2uuelyLbCLWuJWq_axuKSZbZvpYeqWtIAde8FjCiO7RPlEc0nyzWBst8RBxQ-Bekg9UXPhxBRcm0HwA.Q2cBSFOQAC-QKDwmjrQXnVQd3jNOppMl9oZfd2yuKeY'; + dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + responseHeader['Akamai-DAP-Token'] = encMembership; + request.respond(200, responseHeader, encMembership); + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) + expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(1643830630); + }); + + it('test dapRefreshEncryptedMembership error response', function () { + dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(400, responseHeader, 'error'); + expect(config.getConfig().ortb2).to.be.equal(undefined); + }); + + it('test dapRefreshEncryptedMembership 403 error response', function () { + getRealTimeData({}, () => {}, emoduleConfig, {}); + dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(403, responseHeader, 'error'); + let requestTokenize = server.requests[1] + requestTokenize.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 1); + }); + }); + + describe('dapGetEncryptedMembershipFromLocalStorage test', function () { + it('test dapGetEncryptedMembershipFromLocalStorage function with valid cache', function () { + localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)) + expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.equal(JSON.stringify(encMembership)); + }); + + it('test dapGetEncryptedMembershipFromLocalStorage function with invalid cache', function () { + let expiry = Math.round(Date.now() / 1000.0) - 100; // in seconds + let encMembership = {'expiry': expiry, 'encryptedSegments': cachedEncryptedMembership.encryptedSegments} + localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(encMembership)) + expect(dapUtils.dapGetEncryptedMembershipFromLocalStorage()).to.equal(null); + }); + }); + + describe('Akamai-DAP-SS-ID test', function () { + it('Akamai-DAP-SS-ID present in response header', function () { + let expiry = Math.round(Date.now() / 1000.0) + 300; // in seconds + dapUtils.dapRefreshToken(sampleConfig, false, onDone) + let request = server.requests[0]; + let sampleSSID = 'Test_SSID_Spec'; + responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; + responseHeader['Akamai-DAP-SS-ID'] = sampleSSID; + request.respond(200, responseHeader, ''); + expect(localStorage.getItem(DAP_SS_ID)).to.be.equal(JSON.stringify(sampleSSID)); + }); + + it('Test if Akamai-DAP-SS-ID is present in request header', function () { + let expiry = Math.round(Date.now() / 1000.0) + 100; // in seconds + localStorage.setItem(DAP_SS_ID, JSON.stringify('Test_SSID_Spec')) + dapUtils.dapRefreshToken(sampleConfig, false, onDone) + let request = server.requests[0]; + let ssidHeader = request.requestHeaders['Akamai-DAP-SS-ID']; + responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; + request.respond(200, responseHeader, ''); + expect(ssidHeader).to.be.equal('Test_SSID_Spec'); }); }); }); From 050a4258d0a659b192eab70c5ef4e2bdc0319a7e Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Wed, 20 Apr 2022 18:07:02 +0530 Subject: [PATCH 2/7] Fixed the timeout issue in build browserstack tests --- modules/akamaiDapRtdProvider.js | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 0d02d400073..963db314a7b 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -23,6 +23,8 @@ export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds export const DAP_MAX_RETRY_TOKENIZE = 1; export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' export const DAP_AUDIO_FP = 'dap_e17' +export const DAP_FONTS_FP = 'dap_e2' +export const DAP_WEBGL_FP = 'dap_e3' export const ENTROPY_EXPIRY = 3600; // in seconds export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); @@ -152,14 +154,12 @@ export const dapUtils = { dapCalculateEntropy: function() { dapUtils.dapGetAudioFp(); + // fonts fp and webgl fp + setTimeout(dapUtils.dapGetFontsAndWebglFp, 0); let entropyDict = {} let entropy = {} // canvas fp entropy.e1 = dapUtils.dapGetCanvasFp(); - // fonts fp - entropy.e2 = dapUtils.dapGetFontsFp(); - // webgl fp - entropy.e3 = dapUtils.dapGetWebglFp(); // misc fp entropy.e4 = window.screen.colorDepth ? JSON.stringify(window.screen.colorDepth) : 'NA'; entropy.e5 = navigator.deviceMemory ? JSON.stringify(navigator.deviceMemory) : 'NA'; @@ -267,6 +267,7 @@ export const dapUtils = { return hashFonts.toString(); } catch (err) { logMessage('Error while calcualting dapGetFontsFp() ' + strOnError); + return 'NC'; } }, @@ -282,7 +283,7 @@ export const dapUtils = { webglContext = canvas.getContext('webgl2') || canvas.getContext('experimental-webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl') || canvas.getContext('moz-webgl'); } catch (e) { logMessage('Exeception occured: ', e); - return; + return 'NC'; } try { @@ -314,17 +315,24 @@ export const dapUtils = { webglContext.vertexAttribPointer(webglProgram.vertexPosAttrib, webglBuffer.itemSize, webglContext.FLOAT, !1, 0, 0); webglContext.uniform2f(webglProgram.offsetUniform, 1, 1); webglContext.drawArrays(webglContext.TRIANGLE_STRIP, 0, webglBuffer.numItems); + var pixels = new Uint8Array(width * height * 4); + webglContext.readPixels(0, 0, width, height, webglContext.RGBA, webglContext.UNSIGNED_BYTE, pixels); + webglString = JSON.stringify(pixels).replace(/,?"[0-9]+":/g, ''); + logMessage('webgl fp', webglString); + const hashWebgl = sha256(webglString); + return hashWebgl.toString(); } } catch (e) { logMessage('Exeception occured: ', e); + return 'NC'; } + }, - var pixels = new Uint8Array(width * height * 4); - webglContext.readPixels(0, 0, width, height, webglContext.RGBA, webglContext.UNSIGNED_BYTE, pixels); - webglString = JSON.stringify(pixels).replace(/,?"[0-9]+":/g, ''); - logMessage('webgl fp', webglString); - const hashWebgl = sha256(webglString); - return hashWebgl.toString(); + dapGetFontsAndWebglFp: function() { + let fontsFp = dapUtils.dapGetFontsFp(); + localStorage.setItem(DAP_FONTS_FP, JSON.stringify(fontsFp)); + let webGlFp = dapUtils.dapGetWebglFp(); + localStorage.setItem(DAP_WEBGL_FP, JSON.stringify(webGlFp)); }, dapGetAudioFp: function(entropy) { @@ -753,6 +761,12 @@ export const dapUtils = { let audioFp = JSON.parse(localStorage.getItem(DAP_AUDIO_FP)); audioFp = audioFp || 'NA'; entropyDict.entropy.e17 = audioFp; + let fontsFp = JSON.parse(localStorage.getItem(DAP_FONTS_FP)); + fontsFp = fontsFp || 'NA'; + entropyDict.entropy.e2 = fontsFp; + let webGlFp = JSON.parse(localStorage.getItem(DAP_WEBGL_FP)); + webGlFp = webGlFp || 'NA'; + entropyDict.entropy.e3 = webGlFp; apiParams.entropy = entropyDict.entropy; } else { logMessage('Entropy not added to Tokenize apiParams.'); From 11fa3fe796271083c5c757f3ca8fde6a252c70af Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Fri, 6 May 2022 20:10:04 +0530 Subject: [PATCH 3/7] Fixing review comments --- .../gpt/akamaidap_segments_example.html | 4 +- modules/akamaiDapRtdProvider.js | 306 +++--------------- modules/akamaiDapRtdProvider.md | 5 +- .../spec/modules/akamaiDapRtdProvider_spec.js | 13 +- 4 files changed, 60 insertions(+), 268 deletions(-) diff --git a/integrationExamples/gpt/akamaidap_segments_example.html b/integrationExamples/gpt/akamaidap_segments_example.html index 450633ed0db..b4e7495002e 100644 --- a/integrationExamples/gpt/akamaidap_segments_example.html +++ b/integrationExamples/gpt/akamaidap_segments_example.html @@ -78,7 +78,9 @@ apiVersion: "x1", domain: "prebid.org", identityType: "dap-signature:1.3.0", - segtax: 504 + segtax: 504, + dapFpUrl: 'https://dap-dist.akamaized.net/dapfingerprinting.js', + dapFpTimeout: 1500 } } ] diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 963db314a7b..2d7ad63e43e 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -4,12 +4,11 @@ * The module will fetch real-time data from DAP * @module modules/akamaiDapRtdProvider * @requires module:modules/realTimeData -*/ + */ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import sha256 from 'crypto-js/sha256'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; const MODULE_NAME = 'realTimeData'; @@ -22,10 +21,6 @@ export const DAP_SS_ID = 'dap_ss_id'; export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds export const DAP_MAX_RETRY_TOKENIZE = 1; export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' -export const DAP_AUDIO_FP = 'dap_e17' -export const DAP_FONTS_FP = 'dap_e2' -export const DAP_WEBGL_FP = 'dap_e3' -export const ENTROPY_EXPIRY = 3600; // in seconds export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); let dapRetryTokenize = 0; @@ -34,7 +29,7 @@ let dapRetryTokenize = 0; * Lazy merge objects. * @param {String} target * @param {String} source -*/ + */ function mergeLazy(target, source) { if (!isPlainObject(target)) { target = {}; @@ -71,11 +66,38 @@ export function addRealTimeData(rtd) { */ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); - if (entropyDict && entropyDict.expires_at > Math.round(Date.now() / 1000.0)) { - logMessage('Using cached entropy'); - } else { - dapUtils.dapCalculateEntropy(); - } + let loadScriptPromise = new Promise((resolve, reject) => { + if (rtdConfig && rtdConfig.params && rtdConfig.params.dapFpTimeout && Number.isInteger(rtdConfig.params.dapFpTimeout)) { + setTimeout(reject, rtdConfig.params.dapFpTimeout, Error('DapFP script could not be loaded')); + } + if (entropyDict && entropyDict.expires_at > Math.round(Date.now() / 1000.0)) { + logMessage('Using cached entropy'); + resolve(); + } else { + if (typeof window.dapCalculateEntropy === 'function') { + window.dapCalculateEntropy(resolve, reject); + } else { + if (rtdConfig && rtdConfig.params && rtdConfig.params.dapFpUrl) { + let fpScript = document.createElement('script'); + fpScript.setAttribute('src', rtdConfig.params.dapFpUrl); + fpScript.onload = () => dapUtils.dapGetEntropy(resolve, reject); + window.document.body.appendChild(fpScript); + } else { + reject(Error('Please check if dapFpUrl is specified under config.params')); + } + } + } + }); + loadScriptPromise + .catch((error) => { + logError('Entropy could not be calculated due to: ', error.message); + }) + .finally(() => { + generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent); + }); +} + +export function generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { logInfo('DEBUG(getRealTimeData) - ENTER'); logMessage(' - apiHostname: ' + rtdConfig.params.apiHostname); logMessage(' - apiVersion: ' + rtdConfig.params.apiVersion); @@ -136,7 +158,7 @@ function callDapAPIs(bidConfig, onDone, rtdConfig, userConsent) { * @param {Object} provider * @param {Object} userConsent * @return {boolean} -*/ + */ function init(provider, userConsent) { return true; } @@ -149,237 +171,12 @@ export const akamaiDapRtdSubmodule = { }; submodule(MODULE_NAME, akamaiDapRtdSubmodule); - export const dapUtils = { - - dapCalculateEntropy: function() { - dapUtils.dapGetAudioFp(); - // fonts fp and webgl fp - setTimeout(dapUtils.dapGetFontsAndWebglFp, 0); - let entropyDict = {} - let entropy = {} - // canvas fp - entropy.e1 = dapUtils.dapGetCanvasFp(); - // misc fp - entropy.e4 = window.screen.colorDepth ? JSON.stringify(window.screen.colorDepth) : 'NA'; - entropy.e5 = navigator.deviceMemory ? JSON.stringify(navigator.deviceMemory) : 'NA'; - entropy.e6 = navigator.cpuClass ? navigator.cpuClass : 'NA' - entropy.e7 = navigator.language ? navigator.language : 'NA'; - entropy.e8 = navigator.cookieEnabled ? JSON.stringify(navigator.cookieEnabled) : 'NA'; - entropy.e9 = navigator.userAgent ? navigator.userAgent : 'NA'; - entropy.e10 = navigator.geoLocation ? navigator.geoLocation : 'NA'; - entropy.e11 = navigator.hardwareConcurrency ? JSON.stringify(navigator.hardwareConcurrency) : 'NA'; - entropy.e12 = window.indexedDB ? JSON.stringify(window.indexedDB) : 'NA'; - entropy.e13 = window.openDatabase ? JSON.stringify(window.openDatabase.length) : 'NA'; - entropy.e14 = navigator.ipAddress ? navigator.ipAddress : 'NA'; - entropy.e15 = navigator.platform ? navigator.platform : 'NA'; - var len = navigator.plugins.length; - var plugins = [] - for (var i = 0; i < len; i++) { - plugins.push(navigator.plugins[i].name); - } - entropy.e16 = sha256(JSON.stringify(plugins, Object.keys(plugins).sort())).toString() || 'NA'; - var pluginsSorted = plugins.sort() - entropy.e18 = sha256(JSON.stringify(pluginsSorted, Object.keys(pluginsSorted).sort())).toString() || 'NA'; - - // Add entropy values along with the expiry to entropyDict and store in localstorage. - entropyDict.entropy = entropy - entropyDict.expires_at = Math.round(Date.now() / 1000.0) + ENTROPY_EXPIRY; - localStorage.setItem(DAP_CLIENT_ENTROPY, JSON.stringify(entropyDict)); - }, - - dapGetCanvasFp: function() { - var strOnError, canvas, strCText, strText, strOut; - - strOnError = 'Error_canvas'; - canvas = null; - strCText = null; - strText = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?"; - strOut = null; - - try { - canvas = document.createElement('canvas'); - strCText = canvas.getContext('2d'); - strCText.textBaseline = 'top'; - strCText.font = "14px 'Arial'"; - strCText.textBaseline = 'alphabetic'; - strCText.fillStyle = '#f60'; - strCText.fillRect(125, 1, 62, 20); - strCText.fillStyle = '#069'; - strCText.fillText(strText, 2, 15); - strCText.fillStyle = 'rgba(102, 204, 0, 0.7)'; - strCText.fillText(strText, 4, 17); - strOut = canvas.toDataURL(); - const hashCanvas = sha256(strOut); - - return hashCanvas.toString(); - } catch (err) { - logMessage('Error while calculating dapGetCanvasFp() ', strOnError); - } - }, - - dapGetFontsFp: function() { - var strOnError, style, fonts, count, template, fragment, divs, i, font, div, body, result, e; - - strOnError = 'Error_fonts'; - style = null; - fonts = null; - font = null; - count = 0; - template = null; - divs = null; - e = null; - div = null; - body = null; - i = 0; - - try { - style = 'position: absolute; visibility: hidden; display: block !important'; - fonts = ['Abadi MT Condensed Light', 'Adobe Fangsong Std', 'Adobe Hebrew', 'Adobe Ming Std', 'Agency FB', 'Aharoni', 'Andalus', 'Angsana New', 'AngsanaUPC', 'Aparajita', 'Arab', 'Arabic Transparent', 'Arabic Typesetting', 'Arial Baltic', 'Arial Black', 'Arial CE', 'Arial CYR', 'Arial Greek', 'Arial TUR', 'Arial', 'Batang', 'BatangChe', 'Bauhaus 93', 'Bell MT', 'Bitstream Vera Serif', 'Bodoni MT', 'Bookman Old Style', 'Braggadocio', 'Broadway', 'Browallia New', 'BrowalliaUPC', 'Calibri Light', 'Calibri', 'Californian FB', 'Cambria Math', 'Cambria', 'Candara', 'Castellar', 'Casual', 'Centaur', 'Century Gothic', 'Chalkduster', 'Colonna MT', 'Comic Sans MS', 'Consolas', 'Constantia', 'Copperplate Gothic Light', 'Corbel', 'Cordia New', 'CordiaUPC', 'Courier New Baltic', 'Courier New CE', 'Courier New CYR', 'Courier New Greek', 'Courier New TUR', 'Courier New', 'DFKai-SB', 'DaunPenh', 'David', 'DejaVu LGC Sans Mono', 'Desdemona', 'DilleniaUPC', 'DokChampa', 'Dotum', 'DotumChe', 'Ebrima', 'Engravers MT', 'Eras Bold ITC', 'Estrangelo Edessa', 'EucrosiaUPC', 'Euphemia', 'Eurostile', 'FangSong', 'Forte', 'FrankRuehl', 'Franklin Gothic Heavy', 'Franklin Gothic Medium', 'FreesiaUPC', 'French Script MT', 'Gabriola', 'Gautami', 'Georgia', 'Gigi', 'Gisha', 'Goudy Old Style', 'Gulim', 'GulimChe', 'GungSeo', 'Gungsuh', 'GungsuhChe', 'Haettenschweiler', 'Harrington', 'Hei S', 'HeiT', 'Heisei Kaku Gothic', 'Hiragino Sans GB', 'Impact', 'Informal Roman', 'IrisUPC', 'Iskoola Pota', 'JasmineUPC', 'KacstOne', 'KaiTi', 'Kalinga', 'Kartika', 'Khmer UI', 'Kino MT', 'KodchiangUPC', 'Kokila', 'Kozuka Gothic Pr6N', 'Lao UI', 'Latha', 'Leelawadee', 'Levenim MT', 'LilyUPC', 'Lohit Gujarati', 'Loma', 'Lucida Bright', 'Lucida Console', 'Lucida Fax', 'Lucida Sans Unicode', 'MS Gothic', 'MS Mincho', 'MS PGothic', 'MS PMincho', 'MS Reference Sans Serif', 'MS UI Gothic', 'MV Boli', 'Magneto', 'Malgun Gothic', 'Mangal', 'Marlett', 'Matura MT Script Capitals', 'Meiryo UI', 'Meiryo', 'Menlo', 'Microsoft Himalaya', 'Microsoft JhengHei', 'Microsoft New Tai Lue', 'Microsoft PhagsPa', 'Microsoft Sans Serif', 'Microsoft Tai Le', 'Microsoft Uighur', 'Microsoft YaHei', 'Microsoft Yi Baiti', 'MingLiU', 'MingLiU-ExtB', 'MingLiU_HKSCS', 'MingLiU_HKSCS-ExtB', 'Miriam Fixed', 'Miriam', 'Mongolian Baiti', 'MoolBoran', 'NSimSun', 'Narkisim', 'News Gothic MT', 'Niagara Solid', 'Nyala', 'PMingLiU', 'PMingLiU-ExtB', 'Palace Script MT', 'Palatino Linotype', 'Papyrus', 'Perpetua', 'Plantagenet Cherokee', 'Playbill', 'Prelude Bold', 'Prelude Condensed Bold', 'Prelude Condensed Medium', 'Prelude Medium', 'PreludeCompressedWGL Black', 'PreludeCompressedWGL Bold', 'PreludeCompressedWGL Light', 'PreludeCompressedWGL Medium', 'PreludeCondensedWGL Black', 'PreludeCondensedWGL Bold', 'PreludeCondensedWGL Light', 'PreludeCondensedWGL Medium', 'PreludeWGL Black', 'PreludeWGL Bold', 'PreludeWGL Light', 'PreludeWGL Medium', 'Raavi', 'Rachana', 'Rockwell', 'Rod', 'Sakkal Majalla', 'Sawasdee', 'Script MT Bold', 'Segoe Print', 'Segoe Script', 'Segoe UI Light', 'Segoe UI Semibold', 'Segoe UI Symbol', 'Segoe UI', 'Shonar Bangla', 'Showcard Gothic', 'Shruti', 'SimHei', 'SimSun', 'SimSun-ExtB', 'Simplified Arabic Fixed', 'Simplified Arabic', 'Snap ITC', 'Sylfaen', 'Symbol', 'Tahoma', 'Times New Roman Baltic', 'Times New Roman CE', 'Times New Roman CYR', 'Times New Roman Greek', 'Times New Roman TUR', 'Times New Roman', 'TlwgMono', 'Traditional Arabic', 'Trebuchet MS', 'Tunga', 'Tw Cen MT Condensed Extra Bold', 'Ubuntu', 'Umpush', 'Univers', 'Utopia', 'Utsaah', 'Vani', 'Verdana', 'Vijaya', 'Vladimir Script', 'Vrinda', 'Webdings', 'Wide Latin', 'Wingdings']; - count = fonts.length; - template = 'ww' + 'ww'; - fragment = document.createDocumentFragment(); - divs = []; - for (i = 0; i < count; i = i + 1) { - font = fonts[i]; - div = document.createElement('div'); - font = font.replace(/['"<>]/g, ''); - div.innerHTML = template.replace(/X/g, font); - div.style.cssText = style; - fragment.appendChild(div); - divs.push(div); - } - body = document.body; - body.insertBefore(fragment, body.firstChild); - result = []; - for (i = 0; i < count; i = i + 1) { - e = divs[i].getElementsByTagName('b'); - if (e[0].offsetWidth === e[1].offsetWidth) { - result.push(fonts[i]); - } - } - // do not combine these two loops, remove child will cause reflow - // and induce severe performance hit - for (i = 0; i < count; i = i + 1) { - body.removeChild(divs[i]); - } - const hashFonts = sha256(result.join('|')); - return hashFonts.toString(); - } catch (err) { - logMessage('Error while calcualting dapGetFontsFp() ' + strOnError); - return 'NC'; - } - }, - - dapGetWebglFp: function() { - var canvas, webglContext; - var width = 48; - var height = 27; - var webglString = ''; - try { - canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - webglContext = canvas.getContext('webgl2') || canvas.getContext('experimental-webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl') || canvas.getContext('moz-webgl'); - } catch (e) { - logMessage('Exeception occured: ', e); - return 'NC'; - } - - try { - if (webglContext) { - var webglBuffer = webglContext.createBuffer(); - webglContext.bindBuffer(webglContext.ARRAY_BUFFER, webglBuffer); - - var size = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]); - webglContext.bufferData(webglContext.ARRAY_BUFFER, size, webglContext.STATIC_DRAW); - webglBuffer.itemSize = 3; - webglBuffer.numItems = 3; - var webglProgram = webglContext.createProgram(); - var vertexShader = webglContext.createShader(webglContext.VERTEX_SHADER); - var vertexShaderSource = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'; - webglContext.shaderSource(vertexShader, vertexShaderSource); - webglContext.compileShader(vertexShader); - - var fragmentShader = webglContext.createShader(webglContext.FRAGMENT_SHADER); - var fragmentShaderSource = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'; - webglContext.shaderSource(fragmentShader, fragmentShaderSource); - webglContext.compileShader(fragmentShader); - webglContext.attachShader(webglProgram, vertexShader); - webglContext.attachShader(webglProgram, fragmentShader); - webglContext.linkProgram(webglProgram); - webglContext.useProgram(webglProgram); - webglProgram.vertexPosAttrib = webglContext.getAttribLocation(webglProgram, 'attrVertex'); - webglProgram.offsetUniform = webglContext.getUniformLocation(webglProgram, 'uniformOffset'); - webglContext.enableVertexAttribArray(webglProgram.vertexPosArray); - webglContext.vertexAttribPointer(webglProgram.vertexPosAttrib, webglBuffer.itemSize, webglContext.FLOAT, !1, 0, 0); - webglContext.uniform2f(webglProgram.offsetUniform, 1, 1); - webglContext.drawArrays(webglContext.TRIANGLE_STRIP, 0, webglBuffer.numItems); - var pixels = new Uint8Array(width * height * 4); - webglContext.readPixels(0, 0, width, height, webglContext.RGBA, webglContext.UNSIGNED_BYTE, pixels); - webglString = JSON.stringify(pixels).replace(/,?"[0-9]+":/g, ''); - logMessage('webgl fp', webglString); - const hashWebgl = sha256(webglString); - return hashWebgl.toString(); - } - } catch (e) { - logMessage('Exeception occured: ', e); - return 'NC'; - } - }, - - dapGetFontsAndWebglFp: function() { - let fontsFp = dapUtils.dapGetFontsFp(); - localStorage.setItem(DAP_FONTS_FP, JSON.stringify(fontsFp)); - let webGlFp = dapUtils.dapGetWebglFp(); - localStorage.setItem(DAP_WEBGL_FP, JSON.stringify(webGlFp)); - }, - - dapGetAudioFp: function(entropy) { - var context = null; - var currentTime = null; - var oscillator = null; - var compressor = null; - - try { - var AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext; - context = new AudioContext(1, 44100, 44100); - currentTime = context.currentTime; - oscillator = context.createOscillator(); - oscillator.type = 'triangle'; - oscillator.frequency.setValueAtTime(10000, currentTime); - - compressor = context.createDynamicsCompressor() - compressor.threshold.value = -50 - compressor.knee.value = 40 - compressor.ratio.value = 12 - compressor.attack.value = 0 - compressor.release.value = 0.25 - - oscillator.connect(compressor); - compressor.connect(context.destination); - - oscillator.start(0); - context.startRendering(); - - context.oncomplete = dapUtils.dapOnCompleteCallback; - } catch (e) { - logMessage('error', e); - } - }, - - dapOnCompleteCallback: function(event) { - var output = null; - for (var i = 4500; i < 5e3; i++) { - var channelData = event.renderedBuffer.getChannelData(0)[i]; - output += Math.abs(channelData); - } - - let fingerprint = output.toString(); - if (fingerprint) { - localStorage.setItem(DAP_AUDIO_FP, JSON.stringify(fingerprint)); + dapGetEntropy: function(resolve, reject) { + if (typeof window.dapCalculateEntropy === 'function') { + window.dapCalculateEntropy(resolve, reject); } else { - localStorage.setItem(DAP_AUDIO_FP, 'NC'); + reject(Error('window.dapCalculateEntropy function is not defined')) } }, @@ -757,19 +554,8 @@ export const dapUtils = { } let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); - if (entropyDict.entropy) { - let audioFp = JSON.parse(localStorage.getItem(DAP_AUDIO_FP)); - audioFp = audioFp || 'NA'; - entropyDict.entropy.e17 = audioFp; - let fontsFp = JSON.parse(localStorage.getItem(DAP_FONTS_FP)); - fontsFp = fontsFp || 'NA'; - entropyDict.entropy.e2 = fontsFp; - let webGlFp = JSON.parse(localStorage.getItem(DAP_WEBGL_FP)); - webGlFp = webGlFp || 'NA'; - entropyDict.entropy.e3 = webGlFp; + if (entropyDict && entropyDict.entropy) { apiParams.entropy = entropyDict.entropy; - } else { - logMessage('Entropy not added to Tokenize apiParams.'); } let method; @@ -878,9 +664,9 @@ export const dapUtils = { return; } let path = '/data-activation/' + - config.api_version + - '/token/' + token + - '/membership'; + config.api_version + + '/token/' + token + + '/membership'; let url = 'https://' + config.api_hostname + path; @@ -961,9 +747,9 @@ export const dapUtils = { return; } let path = '/data-activation/' + - config.api_version + - '/token/' + token + - '/membership/encrypt'; + config.api_version + + '/token/' + token + + '/membership/encrypt'; let url = 'https://' + config.api_hostname + path; diff --git a/modules/akamaiDapRtdProvider.md b/modules/akamaiDapRtdProvider.md index 435744c264f..5e3b93cc5fc 100644 --- a/modules/akamaiDapRtdProvider.md +++ b/modules/akamaiDapRtdProvider.md @@ -17,6 +17,7 @@ ``` pbjs.setConfig({ realTimeData: { + auctionDelay: 2000, dataProviders: [ { name: "dap", @@ -26,7 +27,9 @@ apiVersion: "x1", domain: 'your-domain.com', identityType: 'email' | 'mobile' | ... | 'dap-signature:1.3.0', - segtax: 504 + segtax: 504, + dapFpUrl: 'https://dap-dist.akamaized.net/dapfingerprinting.js', + dapFpTimeout: 1500 // Maximum time for dapFP to run } } ] diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index 9a1e718f6c9..2a6d8a08fcd 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -2,6 +2,7 @@ import {config} from 'src/config.js'; import { dapUtils, getRealTimeData, + generateRealTimeData, akamaiDapRtdSubmodule, storage, DAP_MAX_RETRY_TOKENIZE, DAP_SS_ID, DAP_TOKEN, DAP_MEMBERSHIP, DAP_ENCRYPTED_MEMBERSHIP, } from 'modules/akamaiDapRtdProvider.js'; @@ -132,7 +133,7 @@ describe('akamaiDapRtdProvider', function() { describe('akamaiDapRtdSubmodule', function() { it('successfully instantiates', function () { - expect(akamaiDapRtdSubmodule.init()).to.equal(true); + expect(akamaiDapRtdSubmodule.init()).to.equal(true); }); }); @@ -146,9 +147,9 @@ describe('akamaiDapRtdProvider', function() { let dapGetEncryptedRtdObjStub = sinon.stub(dapUtils, 'dapGetEncryptedRtdObj').returns(cachedEncRtd) expect(config.getConfig().ortb2).to.be.undefined; - getRealTimeData(bidConfig, () => {}, emoduleConfig, {}); + generateRealTimeData(bidConfig, () => {}, emoduleConfig, {}); expect(config.getConfig().ortb2.user.data).to.deep.include.members([encRtdUserObj]); - getRealTimeData(bidConfig, () => {}, cmoduleConfig, {}); + generateRealTimeData(bidConfig, () => {}, cmoduleConfig, {}); expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj]); dapGetRtdObjStub.restore() dapGetMembershipFromLocalStorageStub.restore() @@ -293,7 +294,7 @@ describe('akamaiDapRtdProvider', function() { }; let getDapMembershipStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership); - getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig); + generateRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig); expect(getDapMembershipStub.calledOnce).to.be.equal(true); getDapMembershipStub.restore(); }); @@ -306,7 +307,7 @@ describe('akamaiDapRtdProvider', function() { }; let getDapEncMembershipStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership); - getRealTimeData(testReqBidsConfigObj, onDone, emoduleConfig); + generateRealTimeData(testReqBidsConfigObj, onDone, emoduleConfig); expect(getDapEncMembershipStub.calledOnce).to.be.equal(true); getDapEncMembershipStub.restore(); }); @@ -449,7 +450,7 @@ describe('akamaiDapRtdProvider', function() { }); it('test dapRefreshEncryptedMembership 403 error response', function () { - getRealTimeData({}, () => {}, emoduleConfig, {}); + generateRealTimeData({}, () => {}, emoduleConfig, {}); dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) let request = server.requests[0]; request.respond(403, responseHeader, 'error'); From 1c9e45eac708aa3e53451a2ce780ce59fb23c043 Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Mon, 16 May 2022 23:25:35 +0530 Subject: [PATCH 4/7] Fixing review comments - using storage manager for managing localStorage --- modules/akamaiDapRtdProvider.js | 22 +++++----- .../spec/modules/akamaiDapRtdProvider_spec.js | 40 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 2d7ad63e43e..c4c834aa286 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -65,7 +65,7 @@ export function addRealTimeData(rtd) { * @param {Object} userConsent */ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { - let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); + let entropyDict = JSON.parse(storage.getDataFromLocalStorage(DAP_CLIENT_ENTROPY)); let loadScriptPromise = new Promise((resolve, reject) => { if (rtdConfig && rtdConfig.params && rtdConfig.params.dapFpTimeout && Number.isInteger(rtdConfig.params.dapFpTimeout)) { setTimeout(reject, rtdConfig.params.dapFpTimeout, Error('DapFP script could not be loaded')); @@ -183,7 +183,7 @@ export const dapUtils = { dapGetTokenFromLocalStorage: function(ttl) { let now = Math.round(Date.now() / 1000.0); // in seconds let token = null; - let item = JSON.parse(localStorage.getItem(DAP_TOKEN)); + let item = JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)); if (item) { if (now < item.expires_at) { token = item.token; @@ -206,15 +206,15 @@ export const dapUtils = { item.expires_at = exp - 10; } item.token = token; - localStorage.setItem(DAP_TOKEN, JSON.stringify(item)); + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(item)); dapUtils.dapLog('Successfully updated and stored token; expires at ' + item.expires_at); let dapSSID = xhr.getResponseHeader('Akamai-DAP-SS-ID'); if (dapSSID) { - localStorage.setItem(DAP_SS_ID, JSON.stringify(dapSSID)); + storage.setDataInLocalStorage(DAP_SS_ID, JSON.stringify(dapSSID)); } let deviceId100 = xhr.getResponseHeader('Akamai-DAP-100'); if (deviceId100 != null) { - localStorage.setItem('dap_deviceId100', deviceId100); + storage.setDataInLocalStorage('dap_deviceId100', deviceId100); dapUtils.dapLog('Successfully stored DAP 100 Device ID: ' + deviceId100); } if (refreshMembership) { @@ -235,7 +235,7 @@ export const dapUtils = { dapGetMembershipFromLocalStorage: function() { let now = Math.round(Date.now() / 1000.0); // in seconds let membership = null; - let item = JSON.parse(localStorage.getItem(DAP_MEMBERSHIP)); + let item = JSON.parse(storage.getDataFromLocalStorage(DAP_MEMBERSHIP)); if (item) { if (now < item.expires_at) { membership = { @@ -261,7 +261,7 @@ export const dapUtils = { } item.said = membership.said; item.cohorts = membership.cohorts; - localStorage.setItem(DAP_MEMBERSHIP, JSON.stringify(item)); + storage.setDataInLocalStorage(DAP_MEMBERSHIP, JSON.stringify(item)); dapUtils.dapLog('Successfully updated and stored membership:'); dapUtils.dapLog(item); @@ -284,7 +284,7 @@ export const dapUtils = { dapGetEncryptedMembershipFromLocalStorage: function() { let now = Math.round(Date.now() / 1000.0); // in seconds let encMembership = null; - let item = JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)); + let item = JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)); if (item) { if (now < item.expires_at) { encMembership = { @@ -307,7 +307,7 @@ export const dapUtils = { item.expires_at = exp - 10; } item.encryptedSegments = encToken; - localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(item)); + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(item)); dapUtils.dapLog('Successfully updated and stored encrypted membership:'); dapUtils.dapLog(item); @@ -553,7 +553,7 @@ export const dapUtils = { apiParams.attributes = identity.attributes; } - let entropyDict = JSON.parse(localStorage.getItem(DAP_CLIENT_ENTROPY)); + let entropyDict = JSON.parse(storage.getDataFromLocalStorage(DAP_CLIENT_ENTROPY)); if (entropyDict && entropyDict.entropy) { apiParams.entropy = entropyDict.entropy; } @@ -574,7 +574,7 @@ export const dapUtils = { } let customHeaders = {'Content-Type': 'application/json'}; - let dapSSID = JSON.parse(localStorage.getItem(DAP_SS_ID)); + let dapSSID = JSON.parse(storage.getDataFromLocalStorage(DAP_SS_ID)); if (dapSSID) { customHeaders['Akamai-DAP-SS-ID'] = dapSSID; } diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index 2a6d8a08fcd..59f391db856 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -122,10 +122,10 @@ describe('akamaiDapRtdProvider', function() { beforeEach(function() { config.resetConfig(); - localStorage.removeItem(DAP_TOKEN); - localStorage.removeItem(DAP_MEMBERSHIP); - localStorage.removeItem(DAP_ENCRYPTED_MEMBERSHIP); - localStorage.removeItem(DAP_SS_ID); + storage.removeDataFromLocalStorage(DAP_TOKEN); + storage.removeDataFromLocalStorage(DAP_MEMBERSHIP); + storage.removeDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP); + storage.removeDataFromLocalStorage(DAP_SS_ID); }); afterEach(function () { @@ -140,7 +140,7 @@ describe('akamaiDapRtdProvider', function() { describe('Get Real-Time Data', function() { it('gets rtd from local storage cache', function() { const bidConfig = {}; - localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); let dapGetMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership) let dapGetRtdObjStub = sinon.stub(dapUtils, 'dapGetRtdObj').returns(cachedRtd) let dapGetEncryptedMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership) @@ -214,17 +214,17 @@ describe('akamaiDapRtdProvider', function() { describe('Getting dapTokenize, dapMembership and dapEncryptedMembership from localstorage', function () { it('dapGetTokenFromLocalStorage success', function () { - localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); expect(dapUtils.dapGetTokenFromLocalStorage(60)).to.be.equal(sampleCachedToken.token); }); it('dapGetMembershipFromLocalStorage success', function () { - localStorage.setItem(DAP_MEMBERSHIP, JSON.stringify(cachedMembership)); + storage.setDataInLocalStorage(DAP_MEMBERSHIP, JSON.stringify(cachedMembership)); expect(JSON.stringify(dapUtils.dapGetMembershipFromLocalStorage())).to.be.equal(JSON.stringify(membership)); }); it('dapGetEncryptedMembershipFromLocalStorage success', function () { - localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)); + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)); expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.be.equal(JSON.stringify(encMembership)); }); }); @@ -352,7 +352,7 @@ describe('akamaiDapRtdProvider', function() { let request = server.requests[0]; responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; request.respond(200, responseHeader, JSON.stringify(sampleCachedToken.token)); - expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).token).to.be.equal(sampleCachedToken.token); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).token).to.be.equal(sampleCachedToken.token); }); it('test dapRefreshToken success response with deviceid 100', function () { @@ -360,7 +360,7 @@ describe('akamaiDapRtdProvider', function() { let request = server.requests[0]; responseHeader['Akamai-DAP-100'] = sampleCachedToken.token; request.respond(200, responseHeader, ''); - expect(localStorage.getItem('dap_deviceId100')).to.be.equal(sampleCachedToken.token); + expect(storage.getDataFromLocalStorage('dap_deviceId100')).to.be.equal(sampleCachedToken.token); }); it('test dapRefreshToken success response with exp claim', function () { @@ -369,15 +369,15 @@ describe('akamaiDapRtdProvider', function() { let tokenWithExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQzODMwMzY5fQ..hTbcSQgmmO0HUJJrQ5fRHw.7zjrQXNNVkb-GD0ZhIVhEPcWbyaDBilHTWv-bp1lFZ9mdkSC0QbcAvUbYteiTD7ya23GUwcL2WOW8WgRSHaWHOJe0B5NDqfdUGTzElWfu7fFodRxRgGmwG8Rq5xxteFKLLGHLf1mFYRJKDtjtgajGNUKIDfn9AEt-c5Qz4KU8VolG_KzrLROx-f6Z7MnoPTcwRCj0WjXD6j2D6RAZ80-mKTNIsMIELdj6xiabHcjDJ1WzwtwCZSE2y2nMs451pSYp8W-bFPfZmDDwrkjN4s9ASLlIXcXgxK-H0GsiEbckQOZ49zsIKyFtasBvZW8339rrXi1js-aBh99M7aS5w9DmXPpUDmppSPpwkeTfKiqF0cQiAUq8tpeEQrGDJuw3Qt2.XI8h9Xw-VZj_NOmKtV19wLM63S4snos7rzkoHf9FXCw' responseHeader['Akamai-DAP-Token'] = tokenWithExpiry; request.respond(200, responseHeader, JSON.stringify(tokenWithExpiry)); - expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).expires_at).to.be.equal(1643830359); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).expires_at).to.be.equal(1643830359); }); it('test dapRefreshToken error response', function () { - localStorage.setItem(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); dapUtils.dapRefreshToken(sampleConfig, false, onDone) let request = server.requests[0]; request.respond(400, responseHeader, 'error'); - expect(JSON.parse(localStorage.getItem(DAP_TOKEN)).expires_at).to.be.equal(cacheExpiry);// Since the expiry is same, the token is not updated in the cache + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).expires_at).to.be.equal(cacheExpiry);// Since the expiry is same, the token is not updated in the cache }); }); @@ -398,7 +398,7 @@ describe('akamaiDapRtdProvider', function() { request.respond(200, responseHeader, JSON.stringify(membership)); let rtdObj = dapUtils.dapGetRtdObj(membership, 503) expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); - expect(JSON.parse(localStorage.getItem(DAP_MEMBERSHIP)).expires_at).to.be.equal(1647971548); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_MEMBERSHIP)).expires_at).to.be.equal(1647971548); }); it('test dapRefreshMembership 400 error response', function () { @@ -428,7 +428,7 @@ describe('akamaiDapRtdProvider', function() { request.respond(200, responseHeader, encMembership); let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); - expect(JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(expiry); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(expiry); }); it('test dapRefreshEncryptedMembership success response with exp claim', function () { @@ -439,7 +439,7 @@ describe('akamaiDapRtdProvider', function() { request.respond(200, responseHeader, encMembership); let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) expect(config.getConfig().ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); - expect(JSON.parse(localStorage.getItem(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(1643830630); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(1643830630); }); it('test dapRefreshEncryptedMembership error response', function () { @@ -462,14 +462,14 @@ describe('akamaiDapRtdProvider', function() { describe('dapGetEncryptedMembershipFromLocalStorage test', function () { it('test dapGetEncryptedMembershipFromLocalStorage function with valid cache', function () { - localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)) + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)) expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.equal(JSON.stringify(encMembership)); }); it('test dapGetEncryptedMembershipFromLocalStorage function with invalid cache', function () { let expiry = Math.round(Date.now() / 1000.0) - 100; // in seconds let encMembership = {'expiry': expiry, 'encryptedSegments': cachedEncryptedMembership.encryptedSegments} - localStorage.setItem(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(encMembership)) + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(encMembership)) expect(dapUtils.dapGetEncryptedMembershipFromLocalStorage()).to.equal(null); }); }); @@ -483,12 +483,12 @@ describe('akamaiDapRtdProvider', function() { responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; responseHeader['Akamai-DAP-SS-ID'] = sampleSSID; request.respond(200, responseHeader, ''); - expect(localStorage.getItem(DAP_SS_ID)).to.be.equal(JSON.stringify(sampleSSID)); + expect(storage.getDataFromLocalStorage(DAP_SS_ID)).to.be.equal(JSON.stringify(sampleSSID)); }); it('Test if Akamai-DAP-SS-ID is present in request header', function () { let expiry = Math.round(Date.now() / 1000.0) + 100; // in seconds - localStorage.setItem(DAP_SS_ID, JSON.stringify('Test_SSID_Spec')) + storage.setDataInLocalStorage(DAP_SS_ID, JSON.stringify('Test_SSID_Spec')) dapUtils.dapRefreshToken(sampleConfig, false, onDone) let request = server.requests[0]; let ssidHeader = request.requestHeaders['Akamai-DAP-SS-ID']; From 82824ad8299124da21a683d84c8597b4bcee389f Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Wed, 18 May 2022 13:58:39 +0530 Subject: [PATCH 5/7] Fixing review comments - using loadExternalScript method to load the script --- modules/akamaiDapRtdProvider.js | 21 +++++++++++++++------ src/adloader.js | 3 ++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index c4c834aa286..8bbb38d3947 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -10,9 +10,11 @@ import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; +const MODULE_CODE = 'akamaidap'; export const DAP_TOKEN = 'async_dap_token'; export const DAP_MEMBERSHIP = 'async_dap_membership'; @@ -77,13 +79,10 @@ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { if (typeof window.dapCalculateEntropy === 'function') { window.dapCalculateEntropy(resolve, reject); } else { - if (rtdConfig && rtdConfig.params && rtdConfig.params.dapFpUrl) { - let fpScript = document.createElement('script'); - fpScript.setAttribute('src', rtdConfig.params.dapFpUrl); - fpScript.onload = () => dapUtils.dapGetEntropy(resolve, reject); - window.document.body.appendChild(fpScript); + if (rtdConfig && rtdConfig.params && dapUtils.isValidHttpsUrl(rtdConfig.params.dapFpUrl)) { + loadExternalScript(rtdConfig.params.dapFpUrl, MODULE_CODE, () => { dapUtils.dapGetEntropy(resolve, reject) }); } else { - reject(Error('Please check if dapFpUrl is specified under config.params')); + reject(Error('Please check if dapFpUrl is specified and is valid under config.params')); } } } @@ -453,6 +452,16 @@ export const dapUtils = { logInfo('%cDAP Client', css, args); }, + isValidHttpsUrl: function(urlString) { + let url; + try { + url = new URL(urlString); + } catch (_) { + return false; + } + return url.protocol === 'https:'; + }, + /******************************************************************************* * * V2 (And Beyond) API diff --git a/src/adloader.js b/src/adloader.js index db128c6d7ba..c73f83f300c 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -10,7 +10,8 @@ const _approvedLoadExternalJSList = [ 'adagio', 'browsi', 'brandmetrics', - 'justtag' + 'justtag', + 'akamaidap' ] /** From c8cd3cff7480a61394f090714b011bfc62216b72 Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Thu, 19 May 2022 18:03:28 +0530 Subject: [PATCH 6/7] Fixed unit test case --- test/spec/modules/akamaiDapRtdProvider_spec.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index 59f391db856..33ed61647b5 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -412,9 +412,12 @@ describe('akamaiDapRtdProvider', function() { dapUtils.dapRefreshMembership(sampleConfig, sampleCachedToken.token, onDone) let request = server.requests[0]; request.respond(403, responseHeader, 'error'); - let requestTokenize = server.requests[1] - requestTokenize.respond(403, responseHeader, 'error'); - expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 1); + let requestTokenize = server.requests[1]; + responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; + requestTokenize.respond(200, responseHeader, ''); + let requestMembership = server.requests[2]; + requestMembership.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 2); }); }); @@ -454,9 +457,12 @@ describe('akamaiDapRtdProvider', function() { dapUtils.dapRefreshEncryptedMembership(esampleConfig, sampleCachedToken.token, onDone) let request = server.requests[0]; request.respond(403, responseHeader, 'error'); - let requestTokenize = server.requests[1] - requestTokenize.respond(403, responseHeader, 'error'); - expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 1); + let requestTokenize = server.requests[1]; + responseHeader['Akamai-DAP-Token'] = sampleCachedToken.token; + requestTokenize.respond(200, responseHeader, ''); + let requestMembership = server.requests[2]; + requestMembership.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 2); }); }); From faeb8c5637e8bb541241fae4c54d8351d9ae5768 Mon Sep 17 00:00:00 2001 From: Vikas Srivastava Date: Tue, 24 May 2022 18:06:42 +0530 Subject: [PATCH 7/7] Fixing review comments - Added consent handling --- modules/akamaiDapRtdProvider.js | 28 +++++++++++++-- .../spec/modules/akamaiDapRtdProvider_spec.js | 36 ++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 8bbb38d3947..27d8516f3b9 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -97,7 +97,7 @@ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { } export function generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { - logInfo('DEBUG(getRealTimeData) - ENTER'); + logInfo('DEBUG(generateRealTimeData) - ENTER'); logMessage(' - apiHostname: ' + rtdConfig.params.apiHostname); logMessage(' - apiVersion: ' + rtdConfig.params.apiVersion); dapRetryTokenize = 0; @@ -119,7 +119,7 @@ export function generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent) if (jsonData.rtd) { addRealTimeData(jsonData.rtd); onDone(); - logInfo('DEBUG(getRealTimeData) - 1'); + logInfo('DEBUG(generateRealTimeData) - 1'); // Don't return - ensure the data is always fresh. } } @@ -159,6 +159,9 @@ function callDapAPIs(bidConfig, onDone, rtdConfig, userConsent) { * @return {boolean} */ function init(provider, userConsent) { + if (dapUtils.checkConsent(userConsent) === false) { + return false; + } return true; } @@ -421,7 +424,7 @@ export const dapUtils = { } else { addRealTimeData(data.rtd); } - logInfo('DEBUG(getRealTimeData) - 1'); + logInfo('DEBUG(checkAndAddRealtimeData) - 1'); } }, @@ -462,6 +465,25 @@ export const dapUtils = { return url.protocol === 'https:'; }, + checkConsent: function(userConsent) { + let consent = true; + + if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr; + const hasGdpr = (gdpr && typeof gdpr.gdprApplies === 'boolean' && gdpr.gdprApplies) ? 1 : 0; + const gdprConsentString = hasGdpr ? gdpr.consentString : ''; + if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { + logError('akamaiDapRtd submodule requires consent string to call API'); + consent = false; + } + } else if (userConsent && userConsent.usp) { + const usp = userConsent.usp; + consent = usp[1] !== 'N' && usp[2] !== 'Y'; + } + + return consent; + }, + /******************************************************************************* * * V2 (And Beyond) API diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index 33ed61647b5..25688abd99e 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -1,7 +1,6 @@ import {config} from 'src/config.js'; import { dapUtils, - getRealTimeData, generateRealTimeData, akamaiDapRtdSubmodule, storage, DAP_MAX_RETRY_TOKENIZE, DAP_SS_ID, DAP_TOKEN, DAP_MEMBERSHIP, DAP_ENCRYPTED_MEMBERSHIP, @@ -20,6 +19,18 @@ describe('akamaiDapRtdProvider', function() { const onDone = function() { return true }; + const sampleGdprConsentConfig = { + 'gdpr': { + 'consentString': null, + 'vendorData': {}, + 'gdprApplies': true + } + }; + + const sampleUspConsentConfig = { + 'usp': '1YYY' + }; + const sampleIdentity = { type: 'dap-signature:1.0.0' }; @@ -503,4 +514,27 @@ describe('akamaiDapRtdProvider', function() { expect(ssidHeader).to.be.equal('Test_SSID_Spec'); }); }); + + describe('Test gdpr and usp consent handling', function () { + it('Gdpr applies and gdpr consent string not present', function () { + expect(akamaiDapRtdSubmodule.init(null, sampleGdprConsentConfig)).to.equal(false); + }); + + it('Gdpr applies and gdpr consent string is present', function () { + sampleGdprConsentConfig.gdpr.consentString = 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='; + expect(akamaiDapRtdSubmodule.init(null, sampleGdprConsentConfig)).to.equal(true); + }); + + it('USP consent present and user have opted out', function () { + expect(akamaiDapRtdSubmodule.init(null, sampleUspConsentConfig)).to.equal(false); + }); + + it('USP consent present and user have not been provided with option to opt out', function () { + expect(akamaiDapRtdSubmodule.init(null, {'usp': '1NYY'})).to.equal(false); + }); + + it('USP consent present and user have not opted out', function () { + expect(akamaiDapRtdSubmodule.init(null, {'usp': '1YNY'})).to.equal(true); + }); + }); });