mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-05 00:57:59 +01:00
208 lines
5.2 KiB
TypeScript
208 lines
5.2 KiB
TypeScript
/**
|
|
* FP version 4
|
|
*
|
|
* Not reliant on deprecated properties, and potentially
|
|
* more accurate at what it's supposed to do.
|
|
*/
|
|
|
|
import store from './utils/store';
|
|
|
|
const storageKey = 'cached_ses_value';
|
|
|
|
declare global {
|
|
interface Keyboard {
|
|
getLayoutMap: () => Promise<Map<string, string>>;
|
|
}
|
|
|
|
interface UserAgentData {
|
|
brands: [{ brand: string; version: string }];
|
|
mobile: boolean;
|
|
platform: string;
|
|
}
|
|
|
|
interface Navigator {
|
|
deviceMemory: number | undefined;
|
|
keyboard: Keyboard | undefined;
|
|
userAgentData: UserAgentData | undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a 53-bit non-cryptographic hash of a string.
|
|
*
|
|
* @param str The string to hash.
|
|
* @param seed The seed to use for hash generation.
|
|
* @return The resulting hash as a 53-bit number.
|
|
* @see {@link https://stackoverflow.com/a/52171480}
|
|
*/
|
|
function cyrb53(str: string, seed: number = 0x16fe7b0a): number {
|
|
let h1 = 0xdeadbeef ^ seed;
|
|
let h2 = 0x41c6ce57 ^ seed;
|
|
|
|
for (let i = 0, ch; i < str.length; i++) {
|
|
ch = str.charCodeAt(i);
|
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
}
|
|
|
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
|
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
}
|
|
|
|
/**
|
|
* Get keyboard layout data from the navigator layout map.
|
|
*
|
|
* @return String containing layout map entries, or `none` when unavailable
|
|
*/
|
|
async function getKeyboardData(): Promise<string> {
|
|
if (navigator.keyboard) {
|
|
const layoutMap = await navigator.keyboard.getLayoutMap();
|
|
|
|
return Array.from(layoutMap.entries())
|
|
.sort()
|
|
.map(([k, v]) => `${k}${v}`)
|
|
.join('');
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
/**
|
|
* Get an approximation of memory available in gigabytes.
|
|
*
|
|
* @return String containing device memory data, or `1` when unavailable
|
|
*/
|
|
function getMemoryData(): string {
|
|
if (navigator.deviceMemory) {
|
|
return navigator.deviceMemory.toString();
|
|
}
|
|
|
|
return '1';
|
|
}
|
|
|
|
/**
|
|
* Get the "brands" of the user agent.
|
|
*
|
|
* For Chromium-based browsers this returns additional data like "Edge" or "Chrome"
|
|
* which may also contain additional data beyond the `userAgent` property.
|
|
*
|
|
* @return String containing brand data, or `none` when unavailable
|
|
*/
|
|
function getUserAgentBrands(): string {
|
|
const data = navigator.userAgentData;
|
|
|
|
if (data) {
|
|
let brands = 'none';
|
|
|
|
if (data.brands && data.brands.length > 0) {
|
|
// NB: Chromium implements GREASE protocol to prevent ossification of
|
|
// the "Not a brand" string - see https://stackoverflow.com/a/64443187
|
|
brands = data.brands
|
|
.filter(e => !e.brand.match(/.*ot.*rand.*/gi))
|
|
.map(e => `${e.brand}${e.version}`)
|
|
.sort()
|
|
.join('');
|
|
}
|
|
|
|
return `${brands}${data.mobile}${data.platform}`;
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
/**
|
|
* Get the size in rems of the default body font.
|
|
*
|
|
* Causes a forced layout. Be sure to cache this value.
|
|
*
|
|
* @return String with the rem size
|
|
*/
|
|
function getFontRemSize(): string {
|
|
const testElement = document.createElement('span');
|
|
testElement.style.minWidth = '1rem';
|
|
testElement.style.maxWidth = '1rem';
|
|
testElement.style.position = 'absolute';
|
|
|
|
document.body.appendChild(testElement);
|
|
|
|
const width = testElement.clientWidth.toString();
|
|
|
|
document.body.removeChild(testElement);
|
|
|
|
return width;
|
|
}
|
|
|
|
/**
|
|
* Create a semi-unique string from browser attributes.
|
|
*
|
|
* @return Hexadecimally encoded 53 bit number padded to 7 bytes.
|
|
*/
|
|
async function createFp(): Promise<string> {
|
|
const prints: string[] = [
|
|
navigator.userAgent,
|
|
navigator.hardwareConcurrency.toString(),
|
|
navigator.maxTouchPoints.toString(),
|
|
navigator.language,
|
|
await getKeyboardData(),
|
|
getMemoryData(),
|
|
getUserAgentBrands(),
|
|
getFontRemSize(),
|
|
|
|
screen.height.toString(),
|
|
screen.width.toString(),
|
|
screen.colorDepth.toString(),
|
|
screen.pixelDepth.toString(),
|
|
|
|
window.devicePixelRatio.toString(),
|
|
new Date().getTimezoneOffset().toString(),
|
|
];
|
|
|
|
return cyrb53(prints.join('')).toString(16).padStart(14, '0');
|
|
}
|
|
|
|
/**
|
|
* Gets the existing `_ses` value from local storage or cookies.
|
|
*
|
|
* @return String `_ses` value or `null`
|
|
*/
|
|
function getSesValue(): string | null {
|
|
// Try storage
|
|
const storageValue: string | null = store.get(storageKey);
|
|
if (storageValue) {
|
|
return storageValue;
|
|
}
|
|
|
|
// Try cookie
|
|
const match = document.cookie.match(/_ses=([a-f0-9]+)/);
|
|
if (match && match[1]) {
|
|
return match[1];
|
|
}
|
|
|
|
// Not found
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets the `_ses` cookie.
|
|
*
|
|
* If `cached_ses_value` is present in local storage, uses it to set the `_ses` cookie.
|
|
* Otherwise, if the `_ses` cookie already exists, uses its value instead.
|
|
* Otherwise, attempts to generate a new value for the `_ses` cookie based on
|
|
* various browser attributes.
|
|
* Failing all previous methods, sets the `_ses` cookie to a fallback value.
|
|
*/
|
|
export async function setSesCookie() {
|
|
let sesValue = getSesValue();
|
|
|
|
if (!sesValue || sesValue.charAt(0) !== 'd' || sesValue.length !== 15) {
|
|
// The prepended 'd' acts as a crude versioning mechanism.
|
|
sesValue = `d${await createFp()}`;
|
|
store.set(storageKey, sesValue);
|
|
}
|
|
|
|
document.cookie = `_ses=${sesValue}; path=/; SameSite=Lax`;
|
|
}
|