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_concurrency': 10,
},
'https_key': 'https_key.pem',
'https_cert': 'https_cert.pem',
'dns': [],
'dns_expire': 3600,
'timeout': timeout,

@ -30,6 +30,9 @@ export class Config {
is_client = null;
is_server = null;
https_key = null;
https_cert = null;
timeout = null;
redirect = null;
@ -66,6 +69,7 @@ export class Config {
stores = [];
constructor(json) {
// service
const { protocol, hostname, port, pathname, searchParams, username, password } = new URL(json.service);
this.local_protocol = protocol.slice(0, -1);
this.local_username = username;
@ -75,6 +79,7 @@ export class Config {
this.local_port = +port;
this.local_pathname = pathname;
// server
let server;
// TODO(remove in next version): make backwards compatibility to "json.servers"
if (json.servers !== undefined) {
@ -105,8 +110,15 @@ export class Config {
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.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]"');
}
const { protocol: _protocol, hostname, port, searchParams } = new URL(json.service);
const { protocol, hostname, port, searchParams } = new URL(json.service);
// service.protocol
if (typeof _protocol !== 'string') {
if (typeof protocol !== 'string') {
throw Error('service.protocol is invalid');
}
const protocol = _protocol.slice(0, -1);
const proto = protocol.slice(0, -1);
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(', ')}`);
}
@ -268,7 +280,7 @@ export class Config {
}
// service.query
if (protocol === 'tcp') {
if (proto === 'tcp') {
const forward = searchParams.get('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
let server;
// TODO(remove in next version): make backwards compatibility to "json.servers"

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

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