This commit is contained in:
2026-09-12 21:55:40 +02:00
parent 53055ea5c6
commit 90860b9279
24 changed files with 1958 additions and 45 deletions
+138 -2
View File
@@ -83,6 +83,102 @@
this.rawPub = null;
this.rawSeed = null;
this.isSessionReady = false;
this.hwFingerprint = null;
}
/**
* Compute a hardware-level fingerprint based on GPU, CPU, RAM, screen, and Web Audio DSP.
* Survives across private browsing windows and different browsers on the same physical device.
*/
async getHardwareFingerprint() {
if (this.hwFingerprint) return this.hwFingerprint;
try {
let glVendor = '';
let glRenderer = '';
let glLimits = '';
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
const ext = gl.getExtension('WEBGL_debug_renderer_info');
if (ext) {
glVendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) || '';
glRenderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || '';
}
glLimits = [
gl.getParameter(gl.MAX_TEXTURE_SIZE) || 0,
gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || 0,
gl.getParameter(gl.MAX_VERTEX_ATTRIBS) || 0,
gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS) || 0,
gl.getParameter(gl.MAX_VARYING_VECTORS) || 0,
gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS) || 0
].join(',');
}
} catch (e) {}
const concurrency = navigator.hardwareConcurrency || 0;
const memory = navigator.deviceMemory || 0;
const screenInfo = [
window.screen ? window.screen.width : 0,
window.screen ? window.screen.height : 0,
window.screen ? window.screen.colorDepth : 0,
window.screen ? window.screen.pixelDepth : 0,
window.devicePixelRatio || 1
].join('x');
let audioFp = '';
try {
const AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
if (AudioContext) {
const actx = new AudioContext(1, 44100, 44100);
const osc = actx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(10000, actx.currentTime);
const comp = actx.createDynamicsCompressor();
comp.threshold.setValueAtTime(-50, actx.currentTime);
comp.knee.setValueAtTime(40, actx.currentTime);
comp.ratio.setValueAtTime(12, actx.currentTime);
comp.reduction.setValueAtTime(-20, actx.currentTime);
comp.attack.setValueAtTime(0, actx.currentTime);
comp.release.setValueAtTime(0.25, actx.currentTime);
osc.connect(comp);
comp.connect(actx.destination);
osc.start(0);
const renderedBuffer = await actx.startRendering();
const channel = renderedBuffer.getChannelData(0);
let sum = 0;
const end = Math.min(channel.length, 5000);
for (let i = 4500; i < end; i++) {
sum += Math.abs(channel[i] || 0);
}
audioFp = sum.toFixed(7);
}
} catch (e) {}
const rawHardware = [
glVendor,
glRenderer,
glLimits,
concurrency,
memory,
screenInfo,
audioFp
].join('~~~');
const encoder = new TextEncoder();
const data = encoder.encode(rawHardware);
const hashBuf = await window.crypto.subtle.digest('SHA-256', data);
const hashHex = bytesToHex(new Uint8Array(hashBuf));
this.hwFingerprint = `HW:${hashHex}`;
return this.hwFingerprint;
} catch (err) {
console.warn('[ANON_SSH] Failed to compute hardware fingerprint:', err);
return null;
}
}
getDomain() {
@@ -430,6 +526,8 @@
const timestamp = Date.now();
const message = `anon-auth:${timestamp}:${this.pubkey}`;
const signature = await this.sign(message);
const tombstone = this.getTombstone();
const hwFingerprint = await this.getHardwareFingerprint();
const res = await fetch('/api/v2/anon/session', {
method: 'POST',
@@ -437,11 +535,27 @@
body: JSON.stringify({
pubkey: this.pubkey,
timestamp: timestamp,
signature: signature
signature: signature,
tombstone: tombstone,
hw_fingerprint: hwFingerprint
})
});
const data = await res.json();
if (data.banned) {
this.setTombstone({
banned: true,
fingerprint: data.fingerprint || this.fingerprint,
hw_fingerprint: data.hw_fingerprint || hwFingerprint,
reason: data.reason,
expires: data.expires
});
if (window.location.pathname !== '/banned') {
window.location.href = data.redirect || '/banned';
}
return;
}
if (data.success) {
this.isSessionReady = true;
if (data.csrf_token) {
@@ -452,7 +566,8 @@
window.f0ckAnonIdentity = {
userId: data.user_id,
fingerprint: data.fingerprint,
shortFingerprint: data.short_fingerprint
shortFingerprint: data.short_fingerprint,
hwFingerprint: data.hw_fingerprint || hwFingerprint
};
window.dispatchEvent(new CustomEvent('f0ck:anon_session_ready', { detail: window.f0ckAnonIdentity }));
}
@@ -461,6 +576,27 @@
}
}
/**
* Get client ban tombstone if previously banned.
*/
getTombstone() {
try {
const raw = localStorage.getItem('f0ck_anon_tombstone');
return raw ? JSON.parse(raw) : null;
} catch (e) {
return null;
}
}
/**
* Save client ban tombstone.
*/
setTombstone(tombstone) {
try {
localStorage.setItem('f0ck_anon_tombstone', JSON.stringify(tombstone));
} catch (e) {}
}
/**
* Trigger browser download of id_ed25519 private key file
*/
+48 -2
View File
@@ -1289,6 +1289,33 @@
}
};
const enforceSidebarRecommendationDistribution = (items) => {
if (!Array.isArray(items) || items.length <= 3) return items;
// Collect indices of personalized recommendations in the first 3 items
const recIndicesInFirst3 = [];
for (let i = 0; i < Math.min(3, items.length); i++) {
if (items[i] && items[i].personalized) recIndicesInFirst3.push(i);
}
// If more than 1 recommendation in first 3, swap extras with non-rec items from index >= 3
if (recIndicesInFirst3.length > 1) {
let nonRecIdx = 3;
for (let r = 1; r < recIndicesInFirst3.length; r++) {
const swapSlot = recIndicesInFirst3[r];
while (nonRecIdx < items.length && items[nonRecIdx] && items[nonRecIdx].personalized) {
nonRecIdx++;
}
if (nonRecIdx < items.length) {
const tmp = items[swapSlot];
items[swapSlot] = items[nonRecIdx];
items[nonRecIdx] = tmp;
nonRecIdx++;
}
}
}
return items;
};
const loadRecommendations = async (silent = false) => {
const container = document.getElementById('sidebar-recommendations-container');
if (!container || recommendationsLoading) return;
@@ -1316,6 +1343,7 @@
const items = data.items || data.videos || [];
if (data.success && items.length > 0) {
enforceSidebarRecommendationDistribution(items);
currentRecommendations = items;
recommendationsLoaded = true;
@@ -1368,7 +1396,7 @@
const affParams = getSessionAffinityParams();
const mime = getCurrentMimeFilter();
const mimeParam = mime ? `&mime=${encodeURIComponent(mime)}` : '';
const res = await fetch(`/api/v2/recommendations?limit=15&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
const res = await fetch(`/api/v2/recommendations?limit=15&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}&continuation=1${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
@@ -1877,10 +1905,28 @@
}
const excludeArr = Array.from(excludeSet).slice(-120);
const cards = Array.from(container ? container.querySelectorAll('.sidebar-video-card') : []);
const cardIndex = cards.indexOf(card);
let preferParam = '';
if (cardIndex >= 0 && cardIndex < 3) {
// Keep exactly 1 recommendation in the first 3 cards
const isPersonalized = card.dataset.personalized === 'true';
preferParam = `&prefer_personalized=${isPersonalized ? 'true' : 'false'}`;
} else if (cardIndex >= 3) {
// Ensure recommendations remain sporadic (never consecutive)
const prevCard = cards[cardIndex - 1];
const nextCard = cards[cardIndex + 1];
const prevIsRec = prevCard && prevCard.dataset.personalized === 'true';
const nextIsRec = nextCard && nextCard.dataset.personalized === 'true';
if (prevIsRec || nextIsRec) {
preferParam = '&prefer_personalized=false';
}
}
const affParams = getSessionAffinityParams();
const mime = getCurrentMimeFilter();
const mimeParam = mime ? `&mime=${encodeURIComponent(mime)}` : '';
const res = await fetch(`/api/v2/recommendations?limit=1&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}${affParams}`, {
const res = await fetch(`/api/v2/recommendations?limit=1&mode=${mode}${mimeParam}&exclude_ids=${excludeArr.join(',')}${preferParam}${affParams}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();