@softvisio/core
Version:
Softisio core
587 lines (466 loc) • 16.6 kB
JavaScript
import "#lib/stream";
import { resolve4 } from "#lib/dns";
import IpAddress from "#lib/ip/address";
import mixins from "#lib/mixins";
import { connect, getDefaultPort } from "#lib/net";
import Mutex from "#lib/threads/mutex";
import ProxyClient from "../client.js";
import OptionsCountry from "../mixins/country.js";
import OptionsLocalAddress from "../mixins/local-address.js";
import OptionsRemoteAddress from "../mixins/remote-address.js";
const SOCKS5_ERROR = {
"1": "General failure",
"2": "Connection not allowed by ruleset",
"3": "Network unreachable",
"4": "Host unreachable",
"5": "Connection refused by destination host",
"6": "TTL expired",
"7": "Command not supported / protocol error",
"8": "Address type not supported",
};
export default class ProxyClientStatic extends mixins( OptionsCountry, OptionsRemoteAddress, OptionsLocalAddress, ProxyClient ) {
#protocol;
#isHttp = false;
#isSocks5 = false;
#isTls = false;
#mutex = new Mutex();
// properties
get protocol () {
if ( this.#protocol == null ) {
const types = [];
if ( this.isHttp ) types.push( "http" );
if ( this.isSocks5 ) types.push( "socks5" );
if ( this.isTls ) types.push( "tls" );
this.#protocol = types.join( "+" ) + ":";
}
return this.#protocol;
}
get isHttp () {
return this.#isHttp;
}
get isSocks5 () {
return this.#isSocks5;
}
get isTls () {
return this.#isTls;
}
// public
async getRemoteAddress () {
if ( this.remoteAddress ) return this.remoteAddress;
if ( !this.#mutex.tryLock() ) return this.#mutex.wait();
const { "default": fetch } = await import( "#lib/fetch" );
var address;
try {
const res = await fetch( "https://httpbin.softvisio.net/ip", {
"dispatcher": new fetch.Dispatcher( {
"proxy": this,
} ),
} );
if ( !res.ok ) throw new Error();
address = new IpAddress( await res.text() );
// this.remoteAddress = address;
}
catch {}
this.#mutex.unlock( address );
return address;
}
getPlaywrightProxy () {
// impossible to connect to local proxy from playwright
if ( this.isLocal ) return;
// http
if ( this.isHttp ) {
return {
"server": ( this.isTls
? "https://"
: "http://" ) + this.hostname + ":" + this.port,
"username": this.username,
"password": this.password,
};
}
// socks5
else if ( this.isSocks5 && !this.username && !this.password ) {
return {
"server": "socks5://" + this.hostname + ":" + this.port,
};
}
}
getProxy () {
return this;
}
getNextProxy () {
return this;
}
getRandomProxy () {
return this;
}
async connect ( url, { hostnameAddress, connectTimeout, checkCertificate = true } = {} ) {
if ( typeof url === "string" ) url = new URL( url );
var hostname;
if ( this.resolve ) {
if ( hostnameAddress ) {
hostname = hostnameAddress;
}
else {
hostname = await resolve4( hostname );
if ( !hostname ) return Promise.reject( "Unable to resolve hostname" );
}
}
else {
hostname = url.hostname;
}
const port = url.port
? +url.port
: getDefaultPort( url.protocol );
if ( !port ) throw "Port is not specified";
// local proxy
if ( this.isLocal ) {
return connect( {
"host": hostname,
port,
connectTimeout,
"tls": this.isTls,
checkCertificate,
"servername": hostname,
"localAddress": this.hostname,
"family": this.localAddress.isIpV4
? 4
: 6,
} );
}
// http: protocol
else if ( url.protocol === "http:" ) {
// http
if ( this.isHttp ) {
return this.#connectHttp( {
connectTimeout,
checkCertificate,
url,
} );
}
// socks5
else if ( this.isSocks5 ) {
return this.#connectSocks5( {
connectTimeout,
checkCertificate,
hostname,
port,
} );
}
}
// https: protocol
else if ( url.protocol === "https:" ) {
// https
if ( this.isHttp ) {
return this.#connectHttps( {
connectTimeout,
checkCertificate,
hostname,
port,
} );
}
// socks5
else if ( this.isSocks5 ) {
return this.#connectSocks5( {
connectTimeout,
checkCertificate,
hostname,
port,
} );
}
}
// other protocol
else {
// socks5
if ( this.isSocks5 ) {
return this.#connectSocks5( {
connectTimeout,
checkCertificate,
hostname,
port,
} );
}
// https
else if ( this.isHttp ) {
return this.#connectHttps( {
connectTimeout,
checkCertificate,
hostname,
port,
} );
}
}
// error
throw "Unable to create proxy connection";
}
// protected
_init ( url, options = {} ) {
if ( typeof url === "string" ) url = new URL( url );
if ( super._init ) super._init( url, options );
const types = new Set( super.protocol.slice( 0, -1 ).split( "+" ) );
if ( types.has( "http" ) ) this.#isHttp = true;
if ( types.has( "socks5" ) ) this.#isSocks5 = true;
if ( types.has( "tls" ) ) this.#isTls = true;
}
// private
async #connectHttp ( { url, connectTimeout, checkCertificate } ) {
return connect(
{
"host": this.hostname,
"port": this.port,
connectTimeout,
"tls": this.isTls,
checkCertificate,
"servername": this.hostname,
},
socket => this.#patchHttpSocket( socket, url )
);
}
async #connectHttps ( { hostname, port, connectTimeout, checkCertificate } ) {
return connect(
{
"host": this.hostname,
"port": this.port,
connectTimeout,
"tls": this.isTls,
checkCertificate,
"servername": this.hostname,
},
async socket => {
const req = [
//
`CONNECT ${ hostname }:${ port } HTTP/1.1\r\n`,
`Host: ${ this.hostname }\r\n`,
];
if ( this.basicAuth ) req.push( `Proxy-Authorization: Basic ${ this.basicAuth }\r\n` );
req.push( "\r\n" );
socket.write( req.join( "" ) );
var headers = await socket.readHttpHeaders();
if ( !headers ) throw "HTTP headers error";
headers = headers.split( "\r\n" )[ 0 ].split( " " );
const status = +headers[ 1 ],
statusText = headers.slice( 2 ).join( " " );
if ( status === 200 ) {
return;
}
else {
throw statusText;
}
}
);
}
async #connectSocks5 ( { hostname, port, connectTimeout, checkCertificate } ) {
return connect(
{
"host": this.hostname,
"port": this.port,
connectTimeout,
"tls": this.isTls,
checkCertificate,
"servername": this.hostname,
},
socket => this.#createSocks5Tunnel( socket, hostname, port )
);
}
async #createSocks5Tunnel ( socket, hostname, port ) {
// authenticate
if ( this.username !== "" ) {
socket.write( Buffer.from( [ 0x05, 0x01, 0x02 ] ) );
}
// no authentication
else {
socket.write( Buffer.from( [ 0x05, 0x01, 0x00 ] ) );
}
var chunk = await socket.readChunk( 2 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
// not a socks 5 proxy server
if ( chunk[ 0 ] !== 0x05 ) throw "Not a socks5 proxy";
// no acceptable auth method found
if ( chunk[ 1 ] === 0xFF ) {
throw "No auth method supported";
}
// auth is required
else if ( chunk[ 1 ] !== 0x00 ) {
// username / password auth
if ( chunk[ 1 ] === 0x02 ) {
const username = Buffer.from( this.username );
const password = Buffer.from( this.password );
// send username / password auth
const buf = Buffer.concat( [
//
Buffer.from( [ 0x01 ] ),
Buffer.from( [ username.length ] ),
username,
Buffer.from( [ password.length ] ),
password,
] );
socket.write( buf );
chunk = await socket.readChunk( 2 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
// auth rejected
if ( chunk[ 0 ] !== 0x01 || chunk[ 1 ] !== 0x00 ) {
throw "Proxy authentication error";
}
}
// unsupported auth method
else {
throw "Proxy authentication method is not supported";
}
}
// create tunnel
let ip;
try {
ip = new IpAddress( hostname );
}
catch {}
// domain name
if ( !ip ) {
const domainName = Buffer.from( hostname );
const portBuf = Buffer.alloc( 2 );
portBuf.writeUInt16BE( port );
socket.write( Buffer.concat( [
//
Buffer.from( [ 0x05, 0x01, 0x00, 0x03 ] ),
Buffer.from( [ domainName.length ] ),
domainName,
portBuf,
] ) );
}
// ipv4 address
else if ( ip.isIpV4 ) {
const buf = Buffer.alloc( 4 );
buf.writeUInt32BE( ip.value );
const portBuf = Buffer.alloc( 2 );
portBuf.writeUInt16BE( port );
socket.write( Buffer.concat( [
//
Buffer.from( [ 0x05, 0x01, 0x00, 0x01 ] ),
buf,
portBuf,
] ) );
}
// ipv6 address
else if ( ip.isIpV6 ) {
// TODO
throw "IPv6 currently is not supported";
}
chunk = await socket.readChunk( 3 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
// connection error
if ( chunk[ 1 ] !== 0x00 ) throw SOCKS5_ERROR[ chunk[ 1 ] ] || "Unknown error";
// read ip type
chunk = await socket.readChunk( 1 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
// ipV4
if ( chunk[ 0 ] === 0x01 ) {
// read ipV4 addr
chunk = await socket.readChunk( 4 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
}
// ipV6
else if ( chunk[ 0 ] === 0x04 ) {
// read ipV6 addr
chunk = await socket.readChunk( 16 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
}
// invalid IP type
else {
throw "Socks5 protocol error";
}
// read BNDPORT
chunk = await socket.readChunk( 2 ).catch( e => null );
if ( !chunk ) throw "Connection closed";
// connected
socket.removeAllListeners();
return socket;
}
#patchHttpSocket ( socket, url ) {
var write = socket._write.bind( socket ),
buffer = "";
const proxy = this;
socket._write = async function ( chunk, encoding, callback ) {
buffer += chunk.toString( "latin1" );
const idx = buffer.indexOf( "\r\n\r\n" );
// headers block not found
if ( idx === -1 ) {
// max. headers length reached
if ( buffer.length > 4096 ) {
this.destroy( "Max. HTTP headers length reached" );
}
// request more data
else {
callback();
}
return;
}
const headers = buffer.slice( 0, idx ).split( "\r\n" ),
patched = {},
header = headers[ 0 ].split( " " );
if ( !header[ 2 ]?.toLowerCase().startsWith( "http/" ) ) {
this.destroy( "HTTP protocol error" );
return;
}
let hostname = url.hostname;
// patch headers
for ( let n = 1; n < headers.length; n++ ) {
const idx = headers[ n ].indexOf( ":" );
if ( idx === -1 ) continue;
const name = headers[ n ].slice( 0, idx ),
id = name.trim().toLowerCase();
if ( id === "host" ) {
if ( patched[ id ] ) {
headers[ n ] = null;
}
else {
hostname = headers[ n ].slice( idx + 1 ).trim();
headers[ n ] = name + ": " + proxy.hostname;
}
}
else if ( id === "connection" ) {
if ( patched[ id ] ) {
headers[ n ] = null;
}
else {
headers[ n ] = "Connection: close";
}
}
else if ( id === "proxy-authorization" ) {
if ( patched[ id ] ) {
headers[ n ] = null;
}
else {
if ( proxy.basicAuth ) {
headers[ n ] = "Proxy-Authorization: Basic " + proxy.basicAuth;
}
else {
headers[ n ] = null;
}
}
}
patched[ id ] = true;
}
if ( !patched[ "host" ] ) {
headers.push( "Host: " + proxy.hostname );
}
if ( !patched[ "connection" ] ) {
headers.push( "Connection: close" );
}
if ( proxy.basicAuth && !patched[ "proxy-authorization" ] ) {
headers.push( "Proxy-Authorization: Basic " + proxy.basicAuth );
}
header[ 1 ] = url.protocol + "//" + hostname + ( url.port
? ":" + url.port
: "" ) + header[ 1 ];
headers[ 0 ] = header.join( " " );
socket._write = write;
write( headers.filter( header => header ).join( "\r\n" ) + "\r\n\r\n" + buffer.slice( idx + 4 ), encoding, callback );
write = null;
buffer = null;
};
}
}
ProxyClient.register( "http:", ProxyClientStatic );
ProxyClient.register( "http+tls:", ProxyClientStatic );
ProxyClient.register( "socks5:", ProxyClientStatic );
ProxyClient.register( "socks5+tls:", ProxyClientStatic );
ProxyClient.register( "http+socks5:", ProxyClientStatic );
ProxyClient.register( "http+socks5+tls:", ProxyClientStatic );