diff --git a/src/activities/redactor.js b/src/activities/redactor.js index 694a96b2b14..65d14722ce5 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -7,6 +7,7 @@ import { ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD } from './activities.js'; +import { scrubIPv4, scrubIPv6 } from '../utils/ipUtils.js'; export const ORTB_UFPD_PATHS = [ 'data', @@ -21,6 +22,8 @@ export const ORTB_UFPD_PATHS = [ ].map(f => `user.${f}`).concat('device.ext.cdep'); export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; +export const ORTB_IPV4_PATHS = ['device.ip'] +export const ORTB_IPV6_PATHS = ['device.ipv6'] /** * @typedef TransformationRuleDef @@ -157,6 +160,22 @@ export function ortb2TransmitRules(isAllowed = isActivityAllowed) { return Math.round((val + Number.EPSILON) * 100) / 100; } }, + { + name: ACTIVITY_TRANSMIT_PRECISE_GEO, + paths: ORTB_IPV4_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_PRECISE_GEO, isAllowed), + get(val) { + return scrubIPv4(val); + } + }, + { + name: ACTIVITY_TRANSMIT_PRECISE_GEO, + paths: ORTB_IPV6_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_PRECISE_GEO, isAllowed), + get(val) { + return scrubIPv6(val); + } + }, { name: ACTIVITY_TRANSMIT_TID, paths: ['source.tid'], diff --git a/src/utils/ipUtils.js b/src/utils/ipUtils.js new file mode 100644 index 00000000000..600c0e75087 --- /dev/null +++ b/src/utils/ipUtils.js @@ -0,0 +1,52 @@ +export function scrubIPv4(ip) { + if (!ip) { + return null; + } + + const ones = 24; + + let ipParts = ip.split('.').map(Number) + + if (ipParts.length != 4) { + return null; + } + + let mask = []; + for (let i = 0; i < 4; i++) { + let n = Math.max(0, Math.min(8, ones - (i * 8))); + mask.push((0xff << (8 - n)) & 0xff); + } + + let maskedIP = ipParts.map((part, i) => part & mask[i]); + + return maskedIP.join('.'); +} + +export function scrubIPv6(ip) { + if (!ip) { + return null; + } + + const ones = 64; + + let ipParts = ip.split(':').map(part => parseInt(part, 16)); + + ipParts = ipParts.map(part => isNaN(part) ? 0 : part); + while (ipParts.length < 8) { + ipParts.push(0); + } + + if (ipParts.length != 8) { + return null; + } + + let mask = []; + for (let i = 0; i < 8; i++) { + let n = Math.max(0, Math.min(16, ones - (i * 16))); + mask.push((0xffff << (16 - n)) & 0xffff); + } + + let maskedIP = ipParts.map((part, i) => part & mask[i]); + + return maskedIP.map(part => part.toString(16)).join(':'); +} diff --git a/test/spec/activities/redactor_spec.js b/test/spec/activities/redactor_spec.js index f54b2dcfb95..84d98a958a0 100644 --- a/test/spec/activities/redactor_spec.js +++ b/test/spec/activities/redactor_spec.js @@ -1,6 +1,8 @@ import { objectTransformer, ORTB_EIDS_PATHS, ORTB_GEO_PATHS, + ORTB_IPV4_PATHS, + ORTB_IPV6_PATHS, ORTB_UFPD_PATHS, redactorFactory, redactRule } from '../../../src/activities/redactor.js'; @@ -299,6 +301,22 @@ describe('redactor', () => { expect(deepAccess(ortb2, path)).to.eql(allowed ? 1.2345 : 1.23); }) }) + ORTB_IPV4_PATHS.forEach(path => { + it(`should ${allowed ? 'NOT ' : ''} round down ${path}`, () => { + const ortb2 = {}; + deepSetValue(ortb2, path, '192.168.1.1'); + redactor.ortb2(ortb2); + expect(deepAccess(ortb2, path)).to.eql(allowed ? '192.168.1.1' : '192.168.1.0'); + }) + }) + ORTB_IPV6_PATHS.forEach(path => { + it(`should ${allowed ? 'NOT ' : ''} round down ${path}`, () => { + const ortb2 = {}; + deepSetValue(ortb2, path, '2001:0000:130F:0000:0000:09C0:876A:130B'); + redactor.ortb2(ortb2); + expect(deepAccess(ortb2, path)).to.eql(allowed ? '2001:0000:130F:0000:0000:09C0:876A:130B' : '2001:0:130f:0:0:0:0:0'); + }) + }) }); }); }) diff --git a/test/spec/unit/utils/ipUtils_spec.js b/test/spec/unit/utils/ipUtils_spec.js new file mode 100644 index 00000000000..8cd82a8c4fe --- /dev/null +++ b/test/spec/unit/utils/ipUtils_spec.js @@ -0,0 +1,58 @@ +import { scrubIPv4, scrubIPv6 } from '../../../../src/utils/ipUtils' + +describe('ipUtils', () => { + describe('ipv4', () => { + it('should mask ip v4', () => { + let input = '192.168.1.1'; + let output = scrubIPv4(input); + expect(output).to.deep.equal('192.168.1.0'); + input = '192.168.255.255'; + output = scrubIPv4(input); + expect(output).to.deep.equal('192.168.255.0'); + }); + + it('should return null for null input', () => { + let input = null; + let output = scrubIPv4(input); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '192.130.2'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + }); + + describe('ipv6', () => { + it('should mask ip v6', () => { + let input = '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'; + let output = scrubIPv6(input); + expect(output).to.deep.equal('2001:db8:3333:4444:0:0:0:0'); + }); + + it('should return null for null input', () => { + let input = null; + let output = scrubIPv6(input); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '2001:db8:3333:4444:CCCC:DDDD:EEEE'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = 'invalid'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + }); +})