src: add client side https proxy support

This commit is contained in:
Micooz 2018-06-13 20:52:18 +08:00
parent 3116eca5b8
commit 142f8f5d77
4 changed files with 64 additions and 19 deletions

@ -45,6 +45,8 @@ module.exports = function init({ isMinimal, isOverwrite, isDryRun = false }) {
'mux': false, 'mux': false,
'mux_concurrency': 10, 'mux_concurrency': 10,
}, },
'https_key': 'https_key.pem',
'https_cert': 'https_cert.pem',
'dns': [], 'dns': [],
'dns_expire': 3600, 'dns_expire': 3600,
'timeout': timeout, 'timeout': timeout,

@ -30,6 +30,9 @@ export class Config {
is_client = null; is_client = null;
is_server = null; is_server = null;
https_key = null;
https_cert = null;
timeout = null; timeout = null;
redirect = null; redirect = null;
@ -66,6 +69,7 @@ export class Config {
stores = []; stores = [];
constructor(json) { constructor(json) {
// service
const { protocol, hostname, port, pathname, searchParams, username, password } = new URL(json.service); const { protocol, hostname, port, pathname, searchParams, username, password } = new URL(json.service);
this.local_protocol = protocol.slice(0, -1); this.local_protocol = protocol.slice(0, -1);
this.local_username = username; this.local_username = username;
@ -75,6 +79,7 @@ export class Config {
this.local_port = +port; this.local_port = +port;
this.local_pathname = pathname; this.local_pathname = pathname;
// server
let server; let server;
// TODO(remove in next version): make backwards compatibility to "json.servers" // TODO(remove in next version): make backwards compatibility to "json.servers"
if (json.servers !== undefined) { if (json.servers !== undefined) {
@ -105,8 +110,15 @@ export class Config {
this._initServer(server); this._initServer(server);
} }
// common // https_cert, https_key
if (this.is_client && this.local_protocol === 'https') {
logger.info(`[config] loading ${json.https_cert}`);
this.https_cert = loadFileSync(json.https_cert);
logger.info(`[config] loading ${json.https_key}`);
this.https_key = loadFileSync(json.https_key);
}
// common
this.timeout = (json.timeout !== undefined) ? json.timeout * 1e3 : 600 * 1e3; this.timeout = (json.timeout !== undefined) ? json.timeout * 1e3 : 600 * 1e3;
this.dns_expire = (json.dns_expire !== undefined) ? json.dns_expire * 1e3 : DNS_DEFAULT_EXPIRE; this.dns_expire = (json.dns_expire !== undefined) ? json.dns_expire * 1e3 : DNS_DEFAULT_EXPIRE;
@ -242,18 +254,18 @@ export class Config {
throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"'); throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"');
} }
const { protocol: _protocol, hostname, port, searchParams } = new URL(json.service); const { protocol, hostname, port, searchParams } = new URL(json.service);
// service.protocol // service.protocol
if (typeof _protocol !== 'string') { if (typeof protocol !== 'string') {
throw Error('service.protocol is invalid'); throw Error('service.protocol is invalid');
} }
const protocol = _protocol.slice(0, -1); const proto = protocol.slice(0, -1);
const available_client_protocols = [ const available_client_protocols = [
'tcp', 'http', 'socks', 'socks5', 'socks4', 'socks4a', 'tcp', 'http', 'https', 'socks', 'socks5', 'socks4', 'socks4a',
]; ];
if (!available_client_protocols.includes(protocol)) { if (!available_client_protocols.includes(proto)) {
throw Error(`service.protocol must be: ${available_client_protocols.join(', ')}`); throw Error(`service.protocol must be: ${available_client_protocols.join(', ')}`);
} }
@ -268,7 +280,7 @@ export class Config {
} }
// service.query // service.query
if (protocol === 'tcp') { if (proto === 'tcp') {
const forward = searchParams.get('forward'); const forward = searchParams.get('forward');
// ?forward // ?forward
@ -285,6 +297,16 @@ export class Config {
} }
} }
// https_cert, https_key
if (proto === 'https') {
if (typeof json.https_cert !== 'string' || json.https_cert === '') {
throw Error('"https_cert" must be provided');
}
if (typeof json.https_key !== 'string' || json.https_key === '') {
throw Error('"https_key" must be provided');
}
}
// server // server
let server; let server;
// TODO(remove in next version): make backwards compatibility to "json.servers" // TODO(remove in next version): make backwards compatibility to "json.servers"

@ -150,6 +150,7 @@ export class Hub {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const { local_protocol, local_search_params, local_host, local_port } = this._config; const { local_protocol, local_search_params, local_host, local_port } = this._config;
const { local_username: username, local_password: password } = this._config; const { local_username: username, local_password: password } = this._config;
const { https_key, https_cert } = this._config;
let server = null; let server = null;
switch (local_protocol) { switch (local_protocol) {
case 'tcp': { case 'tcp': {
@ -164,10 +165,22 @@ export class Hub {
case 'socks5': case 'socks5':
case 'socks4': case 'socks4':
case 'socks4a': case 'socks4a':
server = socks.createServer({ bindAddress: local_host, bindPort: local_port, username, password }); server = socks.createServer({
bindAddress: local_host,
bindPort: local_port,
username,
password,
});
break; break;
case 'http': case 'http':
server = httpProxy.createServer({ username, password }); case 'https':
server = httpProxy.createServer({
secure: local_protocol === 'https',
https_key,
https_cert,
username,
password,
});
break; break;
default: default:
return reject(Error(`unsupported protocol: "${local_protocol}"`)); return reject(Error(`unsupported protocol: "${local_protocol}"`));

@ -1,5 +1,6 @@
import { URL } from 'url'; import { URL } from 'url';
import http from 'http'; import http from 'http';
import https from 'https';
import { logger, isValidPort } from '../utils'; import { logger, isValidPort } from '../utils';
function checkBasicAuthorization(credentials, { username, password }) { function checkBasicAuthorization(credentials, { username, password }) {
@ -14,8 +15,15 @@ function checkBasicAuthorization(credentials, { username, password }) {
return true; return true;
} }
export function createServer({ username, password }) { export function createServer({ secure, https_key, https_cert, username, password }) {
const server = http.createServer(); let name = secure ? 'https' : 'http';
let server = null;
if (secure) {
server = https.createServer({ key: https_key, cert: https_cert });
} else {
server = http.createServer();
}
const isAuthRequired = username !== '' && password !== ''; const isAuthRequired = username !== '' && password !== '';
// Simple HTTP Proxy // Simple HTTP Proxy
@ -28,7 +36,7 @@ export function createServer({ username, password }) {
if (hostname === null || !isValidPort(_port)) { if (hostname === null || !isValidPort(_port)) {
const remote = `${socket.remoteAddress}:${socket.remotePort}`; const remote = `${socket.remoteAddress}:${socket.remotePort}`;
logger.warn(`[http] drop invalid http request sent from ${remote}`); logger.warn(`[${name}] drop invalid http request sent from ${remote}`);
return res.end(); return res.end();
} }
@ -37,7 +45,7 @@ export function createServer({ username, password }) {
const proxyAuth = headers['proxy-authorization'] || ''; const proxyAuth = headers['proxy-authorization'] || '';
const [type, credentials] = proxyAuth.split(' '); const [type, credentials] = proxyAuth.split(' ');
if (type !== 'Basic' || !checkBasicAuthorization(credentials, { username, password })) { if (type !== 'Basic' || !checkBasicAuthorization(credentials, { username, password })) {
logger.error(`[http] [${appAddress}] authorization failed, type=${type} credentials=${credentials}`); logger.error(`[${name}] [${appAddress}] authorization failed, type=${type} credentials=${credentials}`);
return res.end('HTTP/1.1 401 Unauthorized\r\n\r\n'); return res.end('HTTP/1.1 401 Unauthorized\r\n\r\n');
} }
} }
@ -64,7 +72,7 @@ export function createServer({ username, password }) {
// free to recv from application now // free to recv from application now
socket.resume(); socket.resume();
} },
}); });
}); });
@ -77,7 +85,7 @@ export function createServer({ username, password }) {
if (hostname === null || !isValidPort(port)) { if (hostname === null || !isValidPort(port)) {
const remote = `${socket.remoteAddress}:${socket.remotePort}`; const remote = `${socket.remoteAddress}:${socket.remotePort}`;
logger.warn(`[http] [${appAddress}] drop invalid http CONNECT request sent from ${remote}`); logger.warn(`[${name}] [${appAddress}] drop invalid http CONNECT request sent from ${remote}`);
return socket.destroy(); return socket.destroy();
} }
@ -86,7 +94,7 @@ export function createServer({ username, password }) {
const proxyAuth = req.headers['proxy-authorization'] || ''; const proxyAuth = req.headers['proxy-authorization'] || '';
const [type, credentials] = proxyAuth.split(' '); const [type, credentials] = proxyAuth.split(' ');
if (type !== 'Basic' || !checkBasicAuthorization(credentials, { username, password })) { if (type !== 'Basic' || !checkBasicAuthorization(credentials, { username, password })) {
logger.error(`[http] [${appAddress}] authorization failed, type=${type} credentials=${credentials}`); logger.error(`[${name}] [${appAddress}] authorization failed, type=${type} credentials=${credentials}`);
return socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); return socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
} }
} }
@ -96,14 +104,14 @@ export function createServer({ username, password }) {
port: port, port: port,
onConnected: () => { onConnected: () => {
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
} },
}); });
}); });
// errors // errors
server.on('clientError', (err, socket) => { server.on('clientError', (err, socket) => {
const appAddress = `${socket.remoteAddress}:${socket.remotePort}`; const appAddress = `${socket.remoteAddress || ''}:${socket.remotePort || ''}`;
logger.error(`[http] [${appAddress}] invalid http request: ${err.message}`); logger.error(`[${name}] [${appAddress}] invalid http request: ${err.message}`);
socket.destroy(); socket.destroy();
}); });