202 lines
8.4 KiB
JavaScript
202 lines
8.4 KiB
JavaScript
/** HTTP and request-signing helpers for Flixer's source API. */
|
|
|
|
export const SITE_URL = 'https://flixer.gd';
|
|
export const API_URL = 'https://plsdontscrapemelove.flixer.gd';
|
|
|
|
export const HEADERS = {
|
|
'Accept': 'text/plain',
|
|
'Origin': SITE_URL,
|
|
'Referer': `${SITE_URL}/`,
|
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'
|
|
};
|
|
|
|
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
const SHA256_CONSTANTS = [
|
|
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
|
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
|
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
|
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
|
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
|
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
|
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
|
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
|
|
];
|
|
|
|
export function encodeUtf8(value) {
|
|
const bytes = [];
|
|
for (let index = 0; index < value.length; index++) {
|
|
let codePoint = value.charCodeAt(index);
|
|
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && index + 1 < value.length) {
|
|
const trailing = value.charCodeAt(index + 1);
|
|
if (trailing >= 0xDC00 && trailing <= 0xDFFF) {
|
|
codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + trailing - 0xDC00;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (codePoint < 0x80) bytes.push(codePoint);
|
|
else if (codePoint < 0x800) bytes.push(0xC0 | (codePoint >> 6), 0x80 | (codePoint & 63));
|
|
else if (codePoint < 0x10000) bytes.push(0xE0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
|
else bytes.push(0xF0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 63), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
|
}
|
|
return new Uint8Array(bytes);
|
|
}
|
|
|
|
function rotateRight(value, amount) {
|
|
return (value >>> amount) | (value << (32 - amount));
|
|
}
|
|
|
|
export function sha256(input) {
|
|
const bitLength = input.length * 8;
|
|
const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
|
|
const bytes = new Uint8Array(paddedLength);
|
|
bytes.set(input);
|
|
bytes[input.length] = 0x80;
|
|
|
|
const high = Math.floor(bitLength / 0x100000000);
|
|
const low = bitLength >>> 0;
|
|
const end = paddedLength - 8;
|
|
bytes[end] = (high >>> 24) & 255;
|
|
bytes[end + 1] = (high >>> 16) & 255;
|
|
bytes[end + 2] = (high >>> 8) & 255;
|
|
bytes[end + 3] = high & 255;
|
|
bytes[end + 4] = (low >>> 24) & 255;
|
|
bytes[end + 5] = (low >>> 16) & 255;
|
|
bytes[end + 6] = (low >>> 8) & 255;
|
|
bytes[end + 7] = low & 255;
|
|
|
|
const hash = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
|
|
const words = new Uint32Array(64);
|
|
|
|
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
for (let index = 0; index < 16; index++) {
|
|
const position = offset + index * 4;
|
|
words[index] = ((bytes[position] << 24) | (bytes[position + 1] << 16) |
|
|
(bytes[position + 2] << 8) | bytes[position + 3]) >>> 0;
|
|
}
|
|
for (let index = 16; index < 64; index++) {
|
|
const first = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ (words[index - 15] >>> 3);
|
|
const second = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ (words[index - 2] >>> 10);
|
|
words[index] = (words[index - 16] + first + words[index - 7] + second) >>> 0;
|
|
}
|
|
|
|
let [a, b, c, d, e, f, g, h] = hash;
|
|
for (let index = 0; index < 64; index++) {
|
|
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
const choice = (e & f) ^ (~e & g);
|
|
const temporary1 = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
|
|
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
const temporary2 = (sum0 + majority) >>> 0;
|
|
h = g; g = f; f = e; e = (d + temporary1) >>> 0;
|
|
d = c; c = b; b = a; a = (temporary1 + temporary2) >>> 0;
|
|
}
|
|
|
|
hash[0] = (hash[0] + a) >>> 0;
|
|
hash[1] = (hash[1] + b) >>> 0;
|
|
hash[2] = (hash[2] + c) >>> 0;
|
|
hash[3] = (hash[3] + d) >>> 0;
|
|
hash[4] = (hash[4] + e) >>> 0;
|
|
hash[5] = (hash[5] + f) >>> 0;
|
|
hash[6] = (hash[6] + g) >>> 0;
|
|
hash[7] = (hash[7] + h) >>> 0;
|
|
}
|
|
|
|
const output = new Uint8Array(32);
|
|
for (let index = 0; index < hash.length; index++) {
|
|
output[index * 4] = (hash[index] >>> 24) & 255;
|
|
output[index * 4 + 1] = (hash[index] >>> 16) & 255;
|
|
output[index * 4 + 2] = (hash[index] >>> 8) & 255;
|
|
output[index * 4 + 3] = hash[index] & 255;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function hmacSha256(key, message) {
|
|
let keyBytes = key instanceof Uint8Array ? key : encodeUtf8(String(key));
|
|
if (keyBytes.length > 64) keyBytes = sha256(keyBytes);
|
|
|
|
const messageBytes = message instanceof Uint8Array ? message : encodeUtf8(String(message));
|
|
const inner = new Uint8Array(64 + messageBytes.length);
|
|
const outer = new Uint8Array(96);
|
|
for (let index = 0; index < 64; index++) {
|
|
const value = index < keyBytes.length ? keyBytes[index] : 0;
|
|
inner[index] = value ^ 0x36;
|
|
outer[index] = value ^ 0x5C;
|
|
}
|
|
inner.set(messageBytes, 64);
|
|
outer.set(sha256(inner), 64);
|
|
return sha256(outer);
|
|
}
|
|
|
|
function encodeBase64(bytes) {
|
|
let output = '';
|
|
for (let index = 0; index < bytes.length; index += 3) {
|
|
const secondExists = index + 1 < bytes.length;
|
|
const thirdExists = index + 2 < bytes.length;
|
|
const value = (bytes[index] << 16) | ((secondExists ? bytes[index + 1] : 0) << 8) |
|
|
(thirdExists ? bytes[index + 2] : 0);
|
|
output += BASE64_ALPHABET[(value >>> 18) & 63];
|
|
output += BASE64_ALPHABET[(value >>> 12) & 63];
|
|
output += secondExists ? BASE64_ALPHABET[(value >>> 6) & 63] : '=';
|
|
output += thirdExists ? BASE64_ALPHABET[value & 63] : '=';
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function createNonce() {
|
|
const bytes = new Uint8Array(16);
|
|
for (let index = 0; index < bytes.length; index++) bytes[index] = Math.floor(Math.random() * 256);
|
|
return encodeBase64(bytes).replace(/[/+=]/g, '').slice(0, 22);
|
|
}
|
|
|
|
let serverTimeOffset = 0;
|
|
let serverTimeFetchedAt = 0;
|
|
|
|
async function getServerTimestamp() {
|
|
const now = Date.now();
|
|
if (serverTimeFetchedAt && now - serverTimeFetchedAt < 300000) {
|
|
return Math.floor((now + serverTimeOffset) / 1000);
|
|
}
|
|
|
|
try {
|
|
const startedAt = Date.now();
|
|
const response = await fetch(`${API_URL}/api/time?t=${startedAt}`, {
|
|
headers: { ...HEADERS, 'Cache-Control': 'no-cache' }
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const payload = await response.json();
|
|
if (!payload || typeof payload.timestamp !== 'number') throw new Error('invalid response');
|
|
const finishedAt = Date.now();
|
|
serverTimeOffset = payload.timestamp * 1000 + (finishedAt - startedAt) / 2 - finishedAt;
|
|
serverTimeFetchedAt = finishedAt;
|
|
return Math.floor((finishedAt + serverTimeOffset) / 1000);
|
|
} catch (_) {
|
|
return Math.floor(Date.now() / 1000);
|
|
}
|
|
}
|
|
|
|
export async function createSecureHeaders(apiKey, path) {
|
|
const timestamp = await getServerTimestamp();
|
|
const nonce = createNonce();
|
|
const message = `${apiKey}:${timestamp}:${nonce}:${path}`;
|
|
|
|
return {
|
|
'X-Api-Key': apiKey,
|
|
'X-Request-Timestamp': String(timestamp),
|
|
'X-Request-Nonce': nonce,
|
|
'X-Request-Signature': encodeBase64(hmacSha256(apiKey, message)),
|
|
'X-Client-Fingerprint': 'zerlgt',
|
|
'X-Fingerprint-Lite': 'e9136c41504646444'
|
|
};
|
|
}
|
|
|
|
export async function fetchText(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: { ...HEADERS, ...(options.headers || {}) }
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
|
|
return await response.text();
|
|
}
|