diff --git a/README.md b/README.md index bdf0bf7..27e4ef6 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,10 @@ yarn add @tictactrip/luminator ## Available proxy providers -- [BrightData](https://brightdata.com) +- [BrightData](https://get.brightdata.com/myob1aqbwr9f) - [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? 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(); + }); + }); + }); + }); +}); 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/classes/providers/stormproxies/index.ts b/src/classes/providers/stormproxies/index.ts new file mode 100644 index 0000000..9b165ef --- /dev/null +++ b/src/classes/providers/stormproxies/index.ts @@ -0,0 +1,77 @@ +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); + this.setIp(); + } + + /** + * 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}`), + }; + } +} diff --git a/src/classes/providers/stormproxies/types.ts b/src/classes/providers/stormproxies/types.ts new file mode 100644 index 0000000..7d2c023 --- /dev/null +++ b/src/classes/providers/stormproxies/types.ts @@ -0,0 +1,19 @@ +import { AxiosRequestConfig } from 'axios'; +import { EStrategyMode } from '../base/types'; + +interface IStormProxiesConfig { + axiosConfig?: AxiosRequestConfig; + strategy: TStormProxiesStrategy; +} + +type ProxyMappingIpPort = { + host: string; + port: number; +}; + +type TStormProxiesStrategy = { + mode?: EStrategyMode.CHANGE_IP_EVERY_REQUESTS; + mapping: ProxyMappingIpPort[]; +}; + +export { TStormProxiesStrategy, IStormProxiesConfig, ProxyMappingIpPort }; 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';