From 983aa91d8263c99a32e8c95c72c94cf3fa53c75d Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:27:18 +0100 Subject: [PATCH 1/8] feat: add StormProxies proxy provider implementation --- src/classes/providers/stormproxies/index.ts | 76 +++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/classes/providers/stormproxies/index.ts diff --git a/src/classes/providers/stormproxies/index.ts b/src/classes/providers/stormproxies/index.ts new file mode 100644 index 0000000..a2c7ebd --- /dev/null +++ b/src/classes/providers/stormproxies/index.ts @@ -0,0 +1,76 @@ +import { AxiosInstance, AxiosPromise, AxiosRequestConfig } from 'axios'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { ICreateProxyConfig } from '../base/types'; +import { Base } from '../base'; +import { IStormProxiesConfig, ProxyMappingIpPort, TStormProxiesStrategy } from './types'; + +/** + * @description StormProxies proxy provider. + * @extends {Base} + */ +export class StormProxies extends Base { + public axios: AxiosInstance; + + private readonly strategy: TStormProxiesStrategy; + + constructor(config: IStormProxiesConfig) { + super({ axiosConfig: config.axiosConfig }); + this.strategy = config.strategy; + + this.isHostIpPortMappingProvided(this.strategy.mapping); + } + + /** + * Create and set proxy agents. A different port matches a + * different country / IP. + */ + setIp(): StormProxies { + const { host, port } = this.getRandomIpPort(); + + const { httpAgent, httpsAgent } = this.createProxyAgents({ + port, + host, + }); + + this.axios.defaults.httpsAgent = httpsAgent; + this.axios.defaults.httpAgent = httpAgent; + + return this; + } + + /** + * Picks a random host:port from given mapping. + */ + private getRandomIpPort(): ProxyMappingIpPort { + return this.strategy.mapping[this.randomNumber(0, this.strategy.mapping.length - 1)]; + } + + /** + * Checks ip:port mapping is provided. + */ + private isHostIpPortMappingProvided(mapping: ProxyMappingIpPort[]): void { + if (!mapping.length) { + throw new Error('A {IP}:{PORT} mapping has to be provided.'); + } + } + + /** + * Sends a request. + */ + fetch(axiosRequestConfig: AxiosRequestConfig): AxiosPromise { + this.setIp(); + + return this.sendRequest(axiosRequestConfig); + } + + /** + * Creates both an HTTP and HTTPS proxy + */ + private createProxyAgents(params: ProxyMappingIpPort): ICreateProxyConfig { + return { + httpsAgent: new HttpsProxyAgent(`http://${params.host}:${params.port}`), + httpAgent: new HttpProxyAgent(`http://${params.host}:${params.port}`), + }; + } +} From baed03ed343c91e9161832206003bfd63710e2a5 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:27:30 +0100 Subject: [PATCH 2/8] feat: add types for StormProxies configuration and strategy --- src/classes/providers/stormproxies/types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/classes/providers/stormproxies/types.ts diff --git a/src/classes/providers/stormproxies/types.ts b/src/classes/providers/stormproxies/types.ts new file mode 100644 index 0000000..fa3e271 --- /dev/null +++ b/src/classes/providers/stormproxies/types.ts @@ -0,0 +1,20 @@ +import { AxiosRequestConfig } from 'axios'; +import { IProviderConfig, EStrategyMode } from '../base/types'; + +interface IStormProxiesConfig { + proxy: IProviderConfig; + axiosConfig?: AxiosRequestConfig; + strategy?: TStormProxiesStrategy; +} + +type ProxyMappingIpPort = { + host: string; + port: number; +}; + +type TStormProxiesStrategy = { + mode?: EStrategyMode.CHANGE_IP_EVERY_REQUESTS; + mapping: ProxyMappingIpPort[]; +}; + +export { TStormProxiesStrategy, IStormProxiesConfig, ProxyMappingIpPort }; From ece18de370d2f2de3a1a2cbcab6a88595c74efb2 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:41:02 +0100 Subject: [PATCH 3/8] test: add unit tests for StormProxies class and strategy behavior --- __tests__/unit/stormproxies.ts | 140 +++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 __tests__/unit/stormproxies.ts diff --git a/__tests__/unit/stormproxies.ts b/__tests__/unit/stormproxies.ts new file mode 100644 index 0000000..6192058 --- /dev/null +++ b/__tests__/unit/stormproxies.ts @@ -0,0 +1,140 @@ +import * as nock from 'nock'; +import { EStrategyMode, TStormProxiesStrategy, StormProxies, IStormProxiesConfig } from '../../src'; + +describe('Stormproxies', () => { + const mapping: IStormProxiesConfig['strategy']['mapping'] = [ + { host: '123.32.35.64', port: 12355 }, + { host: '123.32.35.65', port: 12355 }, + { host: '123.32.35.66', port: 12355 }, + { host: '123.32.35.67', port: 12355 }, + { host: '123.32.35.68', port: 12355 }, + { host: '123.32.35.69', port: 12355 }, + { host: '123.32.35.70', port: 12355 }, + ]; + + const strategy: TStormProxiesStrategy = { + mode: EStrategyMode.CHANGE_IP_EVERY_REQUESTS, + mapping, + }; + + describe('#constructor', () => { + describe('when is not given a mapping with a single country', () => { + it('should throw an error if a mapping has been set without any countries', async () => { + let error: Error; + + try { + new StormProxies({ + strategy: { + ...strategy, + mapping: [], + }, + }); + } catch (e) { + error = e; + } + + expect(error).toStrictEqual(new Error('A {IP}:{PORT} mapping has to be provided.')); + }); + }); + }); + + describe('#strategy', () => { + describe('when strategy is set to CHANGE_IP_EVERY_REQUESTS', () => { + let stormProxies: StormProxies; + + beforeEach(() => { + stormProxies = new StormProxies({ strategy: { ...strategy, mapping } }); + }); + + const response1 = { + ip: '184.174.62.231', + country: 'FR', + asn: { + asnum: 9009, + org_name: 'M247 Ltd', + }, + geo: { + city: 'Paris', + region: 'IDF', + region_name: 'Île-de-France', + postal_code: '75014', + latitude: 48.8579, + longitude: 2.3491, + tz: 'Europe/Paris', + lum_city: 'paris', + lum_region: 'idf', + }, + }; + + const response2 = { + ip: '178.171.89.101', + country: 'ES', + asn: { + asnum: 9009, + org_name: 'M247 Ltd', + }, + geo: { + city: 'Madrid', + region: 'MD', + region_name: 'Madrid', + postal_code: '28001', + latitude: 40.4167, + longitude: -3.6838, + tz: 'Europe/Madrid', + lum_city: 'madrid', + lum_region: 'md', + }, + }; + + describe('using HTTP', () => { + it("changes the caller's IP address every request", async () => { + const request1Mock = nock('http://lumtest.com').get('/myip.json').once().reply(200, response1); + const request2Mock = nock('http://lumtest.com').get('/myip.json').once().reply(200, response2); + + const result1 = await stormProxies.fetch({ + method: 'get', + baseURL: 'http://lumtest.com', + url: '/myip.json', + }); + + const result2 = await stormProxies.fetch({ + method: 'get', + baseURL: 'http://lumtest.com', + url: '/myip.json', + }); + + expect(result1.data).toStrictEqual(response1); + expect(result2.data).toStrictEqual(response2); + + request1Mock.done(); + request2Mock.done(); + }); + }); + + describe('using HTTPS', () => { + it("changes the caller's IP address every request", async () => { + const request1Mock = nock('https://lumtest.com').get('/myip.json').once().reply(200, response1); + const request2Mock = nock('https://lumtest.com').get('/myip.json').once().reply(200, response2); + + const result1 = await stormProxies.fetch({ + method: 'get', + baseURL: 'https://lumtest.com', + url: '/myip.json', + }); + + const result2 = await stormProxies.fetch({ + method: 'get', + baseURL: 'https://lumtest.com', + url: '/myip.json', + }); + + expect(result1.data).toStrictEqual(response1); + expect(result2.data).toStrictEqual(response2); + + request1Mock.done(); + request2Mock.done(); + }); + }); + }); + }); +}); From c7868d69a5f4c9c61f14318d88d218c1aaec4325 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:42:27 +0100 Subject: [PATCH 4/8] refactor: update StormProxiesConfig to require strategy and remove proxy type --- src/classes/providers/stormproxies/types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/classes/providers/stormproxies/types.ts b/src/classes/providers/stormproxies/types.ts index fa3e271..7d2c023 100644 --- a/src/classes/providers/stormproxies/types.ts +++ b/src/classes/providers/stormproxies/types.ts @@ -1,10 +1,9 @@ import { AxiosRequestConfig } from 'axios'; -import { IProviderConfig, EStrategyMode } from '../base/types'; +import { EStrategyMode } from '../base/types'; interface IStormProxiesConfig { - proxy: IProviderConfig; axiosConfig?: AxiosRequestConfig; - strategy?: TStormProxiesStrategy; + strategy: TStormProxiesStrategy; } type ProxyMappingIpPort = { From 80f9c8a0b04ff43b74d5d0e3eae0f4c3790750a2 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:42:38 +0100 Subject: [PATCH 5/8] feat: initialize IP settings in StormProxies constructor --- src/classes/providers/stormproxies/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/classes/providers/stormproxies/index.ts b/src/classes/providers/stormproxies/index.ts index a2c7ebd..9b165ef 100644 --- a/src/classes/providers/stormproxies/index.ts +++ b/src/classes/providers/stormproxies/index.ts @@ -19,6 +19,7 @@ export class StormProxies extends Base { this.strategy = config.strategy; this.isHostIpPortMappingProvided(this.strategy.mapping); + this.setIp(); } /** From c5473a9d3bf7f599ca3d038a8d79a383b85eff43 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 15:42:52 +0100 Subject: [PATCH 6/8] fix: correct typo in Shifter description and update exports for StormProxies --- src/classes/providers/shifter/index.ts | 3 +-- src/index.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/classes/providers/shifter/index.ts b/src/classes/providers/shifter/index.ts index 944aba2..16a5e4e 100644 --- a/src/classes/providers/shifter/index.ts +++ b/src/classes/providers/shifter/index.ts @@ -12,12 +12,11 @@ import { } from './types'; /** - * @description Schifter proxy provider. + * @description Shifter proxy provider. * @extends {Base} */ export class Shifter extends Base { public axios: AxiosInstance; - public country: EShifterCountry; private readonly config: IShifterConfig; private readonly strategy: TShifterStrategy; diff --git a/src/index.ts b/src/index.ts index b667819..22e2606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,5 @@ export * from './classes/providers/proxyrack'; export * from './classes/providers/proxyrack/types'; export * from './classes/providers/shifter'; export * from './classes/providers/shifter/types'; +export * from './classes/providers/stormproxies'; +export * from './classes/providers/stormproxies/types'; From dc6c705bd7c4888c6b5e4f08e3952d56c41e1c7a Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 16:28:17 +0100 Subject: [PATCH 7/8] docs: update Shifter link and add StormProxies link in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bdf0bf7..389c945 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ yarn add @tictactrip/luminator - [BrightData](https://brightdata.com) - [Proxyrack](https://www.proxyrack.com) -- [Shifter](https://www.shifter.io) +- [Shifter](https://shifter.io/r/0j72/product/basic-backconnect-proxies/Qzzr) +- [StormProxies](https://stormproxies.com/) ## How to use it? From f9f78cdce758e47ca22602f7a7aef0fd3d367bf7 Mon Sep 17 00:00:00 2001 From: Dimitri DO BAIRRO Date: Tue, 4 Nov 2025 16:31:20 +0100 Subject: [PATCH 8/8] docs: update link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 389c945..27e4ef6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ yarn add @tictactrip/luminator ## Available proxy providers -- [BrightData](https://brightdata.com) +- [BrightData](https://get.brightdata.com/myob1aqbwr9f) - [Proxyrack](https://www.proxyrack.com) - [Shifter](https://shifter.io/r/0j72/product/basic-backconnect-proxies/Qzzr) - [StormProxies](https://stormproxies.com/)