Skip to content
12 changes: 11 additions & 1 deletion src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ module.exports = class Client {
* @prop {boolean} [DEBUG] Enable debug mode
* @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type
* @prop {string} [location] Auth page location (For france: https://fr.tradingview.com/)
* @prop {Object<string, string>} [headers] Custom WebSocket headers
*/

/**
Expand All @@ -228,8 +229,17 @@ module.exports = class Client {
if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG;

const server = clientOptions.server || 'data';
this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?type=chart`, {
const defaultHeaders = {
// eslint-disable-next-line max-len
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
};

this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?from=chart&type=chart`, {
origin: 'https://www.tradingview.com',
headers: { ...defaultHeaders, ...clientOptions.headers },
});

if (clientOptions.token) {
Expand Down
8 changes: 6 additions & 2 deletions src/miscRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,11 @@ module.exports = {
* @param {string} [location] Auth page location (For france: https://fr.tradingview.com/)
* @returns {Promise<User>} Token
*/
async getUser(session, signature = '', location = 'https://www.tradingview.com/') {
async getUser(session, signature = '', location = 'https://www.tradingview.com/', redirectCount = 0) {
if (redirectCount > 5) {
throw new Error('Too many redirects - possible WAF or geo-restriction');
}

const { data, headers } = await axios.get(location, {
headers: {
cookie: genAuthCookies(session, signature),
Expand Down Expand Up @@ -461,7 +465,7 @@ module.exports = {
}

if (headers.location !== location) {
return this.getUser(session, signature, headers.location);
return this.getUser(session, signature, headers.location, redirectCount + 1);
}

throw new Error('Wrong or expired sessionid/signature');
Expand Down
49 changes: 49 additions & 0 deletions tests/getUser-redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect } from 'vitest';

describe('getUser redirect protection', () => {
it('should not loop infinitely on repeated redirects', async () => {
// Monkey-patch axios in the require cache to simulate redirect loop
let callCount = 0;
const axiosMock = {
get: async (url: string) => {
callCount += 1;
const isA = url === 'https://www.tradingview.com/';
return {
data: '<html>no auth here</html>',
headers: {
location: isA
? 'https://www.tradingview.com/accounts/signin/'
: 'https://www.tradingview.com/',
},
};
},
};

const axiosPath = require.resolve('axios');
const originalModule = require.cache[axiosPath];
require.cache[axiosPath] = {
id: axiosPath,
filename: axiosPath,
loaded: true,
exports: axiosMock,
} as any;

const miscPath = require.resolve('../src/miscRequests');
delete require.cache[miscPath];

try {
// eslint-disable-next-line global-require
const misc = require('../src/miscRequests');
await expect(
misc.getUser('fake_session', 'fake_signature'),
).rejects.toThrow('Too many redirects');

expect(callCount).toBeGreaterThan(0);
expect(callCount).toBeLessThanOrEqual(10);
} finally {
if (originalModule) require.cache[axiosPath] = originalModule;
else delete require.cache[axiosPath];
delete require.cache[miscPath];
}
}, 5000);
});
Loading