Skip to content

Commit

Permalink
feat: implement TLS cipher shuffling for Node to reduce 404s (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
karashiiro authored Jul 7, 2024
1 parent a66fb5d commit a6f0ae1
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 4 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFiles: ['dotenv/config']
setupFiles: ['dotenv/config', './test-setup.js'],
};
6 changes: 3 additions & 3 deletions src/_module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { Profile } from './profile';
export type { Profile } from './profile';
export { Scraper } from './scraper';
export { SearchMode } from './search';
export { QueryProfilesResponse, QueryTweetsResponse } from './timeline-v1';
export { Tweet } from './tweets';
export type { QueryProfilesResponse, QueryTweetsResponse } from './timeline-v1';
export type { Tweet } from './tweets';
3 changes: 3 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TwitterAuth } from './auth';
import { ApiError } from './errors';
import { Platform, PlatformExtensions } from './platform';
import { updateCookieJar } from './requests';
import { Headers } from 'headers-polyfill';

Expand Down Expand Up @@ -49,9 +50,11 @@ export async function requestApi<T>(
url: string,
auth: TwitterAuth,
method: 'GET' | 'POST' = 'GET',
platform: PlatformExtensions = new Platform(),
): Promise<RequestApiResult<T>> {
const headers = new Headers();
await auth.installTo(headers, url);
await platform.randomizeCiphers();

let res: Response;
do {
Expand Down
27 changes: 27 additions & 0 deletions src/platform/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PlatformExtensions, genericPlatform } from './platform-interface';

export * from './platform-interface';

declare const PLATFORM_NODE: boolean;
declare const PLATFORM_NODE_JEST: boolean;

export class Platform implements PlatformExtensions {
async randomizeCiphers() {
const platform = await Platform.importPlatform();
await platform?.randomizeCiphers();
}

private static async importPlatform(): Promise<null | PlatformExtensions> {
if (PLATFORM_NODE) {
const { platform } = await import('./node/index.js');
return platform as PlatformExtensions;
} else if (PLATFORM_NODE_JEST) {
// Jest gets unhappy when using an await import here, so we just use require instead.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { platform } = require('./node');
return platform as PlatformExtensions;
}

return genericPlatform;
}
}
11 changes: 11 additions & 0 deletions src/platform/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PlatformExtensions } from '../platform-interface';
import { randomizeCiphers } from './randomize-ciphers';

class NodePlatform implements PlatformExtensions {
randomizeCiphers(): Promise<void> {
randomizeCiphers();
return Promise.resolve();
}
}

export const platform = new NodePlatform();
29 changes: 29 additions & 0 deletions src/platform/node/randomize-ciphers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import tls from 'node:tls';
import { randomBytes } from 'node:crypto';

const ORIGINAL_CIPHERS = tls.DEFAULT_CIPHERS;

// How many ciphers from the top of the list to shuffle.
// The remaining ciphers are left in the original order.
const TOP_N_SHUFFLE = 8;

// Modified variation of https://stackoverflow.com/a/12646864
const shuffleArray = (array: unknown[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = randomBytes(4).readUint32LE() % array.length;
[array[i], array[j]] = [array[j], array[i]];
}

return array;
};

// https://github.com/imputnet/cobalt/pull/574
export const randomizeCiphers = () => {
do {
const cipherList = ORIGINAL_CIPHERS.split(':');
const shuffled = shuffleArray(cipherList.slice(0, TOP_N_SHUFFLE));
const retained = cipherList.slice(TOP_N_SHUFFLE);

tls.DEFAULT_CIPHERS = [...shuffled, ...retained].join(':');
} while (tls.DEFAULT_CIPHERS === ORIGINAL_CIPHERS);
};
16 changes: 16 additions & 0 deletions src/platform/platform-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface PlatformExtensions {
/**
* Randomizes the runtime's TLS ciphers to bypass TLS client fingerprinting, which
* hopefully avoids random 404s on some requests.
*
* **References:**
* - https://github.com/imputnet/cobalt/pull/574
*/
randomizeCiphers(): Promise<void>;
}

export const genericPlatform = new (class implements PlatformExtensions {
randomizeCiphers(): Promise<void> {
return Promise.resolve();
}
})();
2 changes: 2 additions & 0 deletions test-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
globalThis.PLATFORM_NODE = false;
globalThis.PLATFORM_NODE_JEST = true;

0 comments on commit a6f0ae1

Please sign in to comment.