auto-pod
Version:
Automatically choose tunneling or direct connection for cocoapods. As easy as `pod` itself. Help you get rid of GFW of China.
327 lines (261 loc) • 8.59 kB
text/typescript
// Copyright 2019 (c) Karl Cauchy
// Rearranged from https://github.com/oyyd/http-proxy-to-socks
// LICENSE: See at LICENSE.md
// inspired by https://github.com/asluchevskiy/http-to-socks-proxy
import * as fs from 'fs';
import * as http from 'http';
import * as matcher from 'matcher';
import { Socket } from 'net';
import * as url from 'url';
import { SocksClient } from 'socks';
import { SocksProxyType } from 'socks/typings/common/constants';
import { IAuthentication, IProxyInfo, SocksProxyAgent } from './Agent';
import { getLogger } from './Logger';
type Callback = () => IProxyInfo;
interface ISocketInfo {
socket: Socket;
}
export interface IProxy extends url.Url {
socks?: string;
nport?: number;
/**
* Whether use remote DNS instead of local solution.
*/
lookup?: boolean;
version?: number;
authentication?: IAuthentication;
}
export interface IConfig {
proxies: IProxy[];
excludes: string[];
includes: string[];
forceTunneling: boolean;
}
export class ProxyServer extends http.Server {
private proxyList: IProxyInfo[] = [];
private includes: string[] = [];
private excludes: string[] = [];
private forceTunneling: boolean;
constructor(options: IConfig) {
super();
// if (options.socks) {
// // stand alone proxy loging
// this.loadProxy(options.socks);
// } else if (options.socksListFileName) {
// // proxy list loading
// this.loadProxyFile(options.socksListFileName);
// if (options.proxyListReloadTimeout) {
// setInterval(
// () => {
// this.loadProxyFile(options.socksListFileName);
// },
// options.proxyListReloadTimeout * 1000,
// );
// }
// }
this.loadProxies(options.proxies);
this.addListener(
'request',
this.requestListener.bind(this, this.onReadElement),
);
this.addListener(
'connect',
this.connectListener.bind(this, this.onReadElement),
);
this.includes = options.includes;
this.excludes = options.excludes;
this.forceTunneling = options.forceTunneling;
}
public loadProxy(proxyLine: string | IProxy) {
try {
this.proxyList.push(this.parseProxyLine(proxyLine));
} catch (ex) {
getLogger().error(ex.message);
}
}
public loadProxies(proxies: IProxy[]) {
for (const proxy of proxies) {
this.loadProxy(proxy);
}
}
public getProxyObject(host: string,
port: string,
login: string,
password: string): IProxyInfo {
return {
authentication: { username: login || '', password: password || '' },
command: 'connect',
host,
lookup: true,
port: parseInt(port, 10),
version: 5,
};
}
public parseProxyLine(line: string | IProxy): IProxyInfo {
if (typeof line === 'string') {
const proxyInfo = line.split(':');
if (proxyInfo.length !== 4 && proxyInfo.length !== 2) {
throw new Error(`Incorrect proxy line: ${line}`);
}
return this.getProxyObject.apply(this, proxyInfo);
}
// Directly use IProxy.
return {
authentication: line.authentication,
command: 'connect',
host: line.host,
lookup: line.lookup,
port: line.nport,
version: line.version,
};
}
public loadProxyFile(fileName: string) {
getLogger().info(`Loading proxy list from file: ${fileName}`);
fs.readFile(fileName, (err, data) => {
if (err) {
getLogger().error(`Impossible to read the proxy file : ${fileName} error : ${err.message}`);
return;
}
const lines = data.toString().split('\n');
const proxyList = [];
for (let i = 0; i < lines.length; i += 1) {
if (!(lines[i] !== '' && lines[i].charAt(0) !== '#')) {
try {
proxyList.push(this.parseProxyLine(lines[i]));
} catch (ex) {
getLogger().error(ex.message);
}
}
}
this.proxyList = proxyList;
});
}
// ----------- Private functions -----------
private randomElement(array: IProxyInfo[]): IProxyInfo {
return array[Math.floor(Math.random() * array.length)];
}
private onReadElement = (): IProxyInfo => {
return this.randomElement(this.proxyList);
}
private doUseAgent(host: string) {
if (this.forceTunneling) {
return false;
}
// Check if we should use the socksAgent.
const includes = matcher([host], this.includes);
const excludes = matcher([host], this.excludes);
let useAgent = false;
if (includes.length > 0) {
// Then this is considered to be yes.
useAgent = true;
}
if (excludes.length > 0) {
useAgent = false;
}
return useAgent;
}
private requestListener(getProxyInfo: Callback,
request: http.IncomingMessage,
response: http.ServerResponse) {
getLogger().info(`request: ${request.url}`);
const proxy = getProxyInfo();
const ph = url.parse(request.url);
// const port = parseInt(ph.port, 10) || 80;
const socksAgent = new SocksProxyAgent(proxy);
// Check if we should use the socksAgent.
const useAgent = this.doUseAgent(ph.hostname);
const options = {
agent: useAgent ? socksAgent : http.globalAgent,
headers: request.headers,
hostname: ph.hostname,
method: request.method,
path: ph.path,
port: ph.port,
};
const proxyRequest = http.request(options);
request.on('error', (err) => {
getLogger().error(`${err.message}`);
proxyRequest.destroy(err);
});
proxyRequest.on('error', (error) => {
getLogger().error(`${error.message} on proxy ${proxy.host}:${proxy.port}`);
response.writeHead(500);
response.end('Connection error\n');
});
proxyRequest.on('response', (proxyResponse) => {
proxyResponse.pipe(response);
response.writeHead(proxyResponse.statusCode, proxyResponse.headers);
});
request.pipe(proxyRequest);
}
private connectListener(getProxyInfo: Callback,
request: http.IncomingMessage,
socketRequest: Socket,
head) {
getLogger().debug(`connect: ${request.url}`);
const proxy = getProxyInfo();
const ph = url.parse(`http://${request.url}`);
const { hostname: host, port } = ph;
const options = {
command: proxy.command,
destination: { host, port: parseInt(port, 10) },
proxy: {
host: proxy.host,
password: '',
port: proxy.port,
type: proxy.version as SocksProxyType,
userId: '',
},
timeout: proxy.timeout,
type: proxy.version,
};
if (proxy.authentication) {
options.proxy.userId = proxy.authentication.username;
options.proxy.password = proxy.authentication.password;
}
let socket: Socket;
socketRequest.on('error', (err: Error) => {
getLogger().error(`${err.message}`);
if (socket) {
socket.destroy(err);
}
});
// Check if we should use the socksAgent.
const useAgent = this.doUseAgent(host);
function tunneling(error: Error, tSocket: Socket) {
socket = tSocket;
if (error) {
// error in SocksSocket creation
getLogger()
.error(`${error.message} connection creating on ${proxy.host}:${proxy.port}`);
socketRequest.write(`HTTP/${request.httpVersion} 500 Connection error\r\n\r\n`);
return;
}
socket.on('error', (err: Error) => {
getLogger().error(`${err.message}`);
socketRequest.destroy(err);
});
// tunneling to the host
socket.pipe(socketRequest);
socketRequest.pipe(socket);
socket.write(head);
socketRequest.write(`HTTP/${request.httpVersion} 200 Connection established\r\n\r\n`);
socket.resume();
}
if (!useAgent) {
// Direct connection
socket = new Socket();
socket.connect({ host, port: parseInt(port, 10) || 443 }, (error: Error) => {
getLogger().info(`Direct connect: ${request.url}`);
tunneling(error, socket);
});
return;
}
SocksClient.createConnection(options, (error: Error,
// req: http.IncomingMessage,
originalSocket: ISocketInfo) => {
getLogger().info(`Tunneling connect: ${request.url}`);
tunneling(error, originalSocket.socket);
});
}
}