Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions packages/generator-networks-creator/src/record-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,63 @@ const KNOWN_OS_FONTS = {
],
};

function isPositiveNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value > 0;
}

function normalizeScreenViewport(screen: any) {
if (!screen || typeof screen !== 'object') return screen;

const normalized = { ...screen };
const outerWidth = isPositiveNumber(normalized.outerWidth)
? normalized.outerWidth
: normalized.width;
const outerHeight = isPositiveNumber(normalized.outerHeight)
? normalized.outerHeight
: normalized.height;
const screenWidth = isPositiveNumber(normalized.width)
? normalized.width
: outerWidth;
const screenHeight = isPositiveNumber(normalized.height)
? normalized.height
: outerHeight;
const availableHeight = isPositiveNumber(normalized.availHeight)
? normalized.availHeight
: screenHeight;

if (
!isPositiveNumber(normalized.innerWidth) &&
isPositiveNumber(screenWidth) &&
isPositiveNumber(outerWidth)
) {
normalized.innerWidth = Math.min(screenWidth, outerWidth);
}

if (
!isPositiveNumber(normalized.innerHeight) &&
isPositiveNumber(availableHeight) &&
isPositiveNumber(outerHeight)
) {
normalized.innerHeight = Math.min(availableHeight, outerHeight);
}

if (
!isPositiveNumber(normalized.clientWidth) &&
isPositiveNumber(normalized.innerWidth)
) {
normalized.clientWidth = normalized.innerWidth;
}

if (
!isPositiveNumber(normalized.clientHeight) &&
isPositiveNumber(normalized.innerHeight)
) {
normalized.clientHeight = normalized.innerHeight;
}

return normalized;
}

export async function getRecordSchema() {
const robotUserAgents = (await fetch(
'https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json',
Expand Down Expand Up @@ -115,6 +172,12 @@ export async function getRecordSchema() {

return {
...castRecord,
browserFingerprint: {
...castRecord.browserFingerprint,
screen: normalizeScreenViewport(
castRecord.browserFingerprint.screen,
),
},
userAgentProps: {
parsedUserAgent,
isDesktop: !['wearable', 'mobile'].includes(
Expand Down Expand Up @@ -247,10 +310,10 @@ export async function getRecordSchema() {
height: z.number().positive(),
availWidth: z.number().positive(),
availHeight: z.number().positive(),
clientWidth: z.number().nonnegative(),
clientHeight: z.number().nonnegative(),
innerWidth: z.number().nonnegative(),
innerHeight: z.number().nonnegative(),
clientWidth: z.number().positive(),
clientHeight: z.number().positive(),
innerWidth: z.number().positive(),
innerHeight: z.number().positive(),
outerWidth: z.number().positive(),
outerHeight: z.number().positive(),
colorDepth: z.number().positive().optional(),
Expand Down
156 changes: 156 additions & 0 deletions test/generator-networks-creator/record-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, expect, test, vi } from 'vitest';

import { getRecordSchema } from '../../packages/generator-networks-creator/src/record-schema';

vi.mock('node-fetch', () => ({
default: vi.fn(async () => ({
json: async () => [],
})),
}));

const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36';

function createValidRecord() {
return {
requestFingerprint: {
headers: {
'user-agent': userAgent,
},
httpVersion: '2',
},
browserFingerprint: {
webdriver: false,
appVersion: userAgent,
hardwareConcurrency: 8,
multimediaDevices: {
speakers: [],
micros: [],
webcams: [],
},
extraProperties: {},
plugins: [],
mimeTypes: [],
deviceMemory: 8,
oscpu: null,
battery: null,
platform: 'MacIntel',
doNotTrack: null,
audioCodecs: {
ogg: 'probably',
mp3: 'probably',
wav: 'probably',
m4a: 'maybe',
aac: 'probably',
},
videoCodecs: {
ogg: 'probably',
h264: 'probably',
webm: 'probably',
},
language: 'en-US',
languages: ['en-US'],
product: 'Gecko',
appName: 'Netscape',
appCodeName: 'Mozilla',
maxTouchPoints: 0,
productSub: '20030107',
vendor: 'Google Inc.',
vendorSub: '',
videoCard: {
vendor: 'Google Inc. (Intel)',
renderer:
'ANGLE (Intel Inc., Intel(R) HD Graphics 630 OpenGL Engine)',
},
fonts: [],
screen: {
availTop: 25,
availLeft: 0,
pageXOffset: 0,
pageYOffset: 0,
screenX: 0,
hasHDR: false,
width: 1440,
height: 900,
availWidth: 1440,
availHeight: 875,
clientWidth: 1284,
clientHeight: 769,
innerWidth: 1284,
innerHeight: 769,
outerWidth: 1284,
outerHeight: 858,
colorDepth: 24,
pixelDepth: 24,
devicePixelRatio: 2,
},
userAgent,
},
};
}

function withScreenValues(screen: Record<string, number>) {
const record = createValidRecord();
record.browserFingerprint.screen = {
...record.browserFingerprint.screen,
...screen,
};
return record;
}

describe('Record schema', () => {
test('accepts browser records with non-zero viewport dimensions', async () => {
const schema = await getRecordSchema();

expect(schema.safeParse(createValidRecord()).success).toBe(true);
});

test('normalizes zero viewport dimensions before validation', async () => {
const schema = await getRecordSchema();
const result = schema.safeParse(
withScreenValues({
clientWidth: 0,
clientHeight: 0,
innerWidth: 0,
innerHeight: 0,
}),
);

expect(result.success).toBe(true);

if (result.success) {
expect(
result.data.browserFingerprint.screen.innerWidth,
).toBeGreaterThan(0);
expect(
result.data.browserFingerprint.screen.innerHeight,
).toBeGreaterThan(0);
expect(result.data.browserFingerprint.screen.clientWidth).toBe(
result.data.browserFingerprint.screen.innerWidth,
);
expect(result.data.browserFingerprint.screen.clientHeight).toBe(
result.data.browserFingerprint.screen.innerHeight,
);
}
});

test.each(['clientWidth', 'clientHeight', 'innerWidth', 'innerHeight'])(
'rejects unfixable zero %s',
async (dimension) => {
const schema = await getRecordSchema();

expect(
schema.safeParse(
withScreenValues({
outerWidth: 0,
outerHeight: 0,
width: 0,
height: 0,
availHeight: 0,
[dimension]: 0,
}),
).success,
).toBe(false);
},
);
});