gfsd
This commit is contained in:
+138
-2
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user