Skip to content
Merged
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
140 changes: 140 additions & 0 deletions __tests__/unit/stormproxies.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
});
3 changes: 1 addition & 2 deletions src/classes/providers/shifter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 77 additions & 0 deletions src/classes/providers/stormproxies/index.ts
Original file line number Diff line number Diff line change
@@ -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}`),
};
}
}
19 changes: 19 additions & 0 deletions src/classes/providers/stormproxies/types.ts
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';