|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { log } from './logging'; |
|
import { LogVerbosity } from './constants'; |
|
import { getDefaultAuthority } from './resolver'; |
|
import { Socket } from 'net'; |
|
import * as http from 'http'; |
|
import * as tls from 'tls'; |
|
import * as logging from './logging'; |
|
import { |
|
SubchannelAddress, |
|
isTcpSubchannelAddress, |
|
subchannelAddressToString, |
|
} from './subchannel-address'; |
|
import { ChannelOptions } from './channel-options'; |
|
import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; |
|
import { URL } from 'url'; |
|
import { DEFAULT_PORT } from './resolver-dns'; |
|
|
|
const TRACER_NAME = 'proxy'; |
|
|
|
function trace(text: string): void { |
|
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); |
|
} |
|
|
|
interface ProxyInfo { |
|
address?: string; |
|
creds?: string; |
|
} |
|
|
|
function getProxyInfo(): ProxyInfo { |
|
let proxyEnv = ''; |
|
let envVar = ''; |
|
|
|
|
|
|
|
|
|
if (process.env.grpc_proxy) { |
|
envVar = 'grpc_proxy'; |
|
proxyEnv = process.env.grpc_proxy; |
|
} else if (process.env.https_proxy) { |
|
envVar = 'https_proxy'; |
|
proxyEnv = process.env.https_proxy; |
|
} else if (process.env.http_proxy) { |
|
envVar = 'http_proxy'; |
|
proxyEnv = process.env.http_proxy; |
|
} else { |
|
return {}; |
|
} |
|
let proxyUrl: URL; |
|
try { |
|
proxyUrl = new URL(proxyEnv); |
|
} catch (e) { |
|
log(LogVerbosity.ERROR, `cannot parse value of "${envVar}" env var`); |
|
return {}; |
|
} |
|
if (proxyUrl.protocol !== 'http:') { |
|
log( |
|
LogVerbosity.ERROR, |
|
`"${proxyUrl.protocol}" scheme not supported in proxy URI` |
|
); |
|
return {}; |
|
} |
|
let userCred: string | null = null; |
|
if (proxyUrl.username) { |
|
if (proxyUrl.password) { |
|
log(LogVerbosity.INFO, 'userinfo found in proxy URI'); |
|
userCred = `${proxyUrl.username}:${proxyUrl.password}`; |
|
} else { |
|
userCred = proxyUrl.username; |
|
} |
|
} |
|
const hostname = proxyUrl.hostname; |
|
let port = proxyUrl.port; |
|
|
|
|
|
|
|
if (port === '') { |
|
port = '80'; |
|
} |
|
const result: ProxyInfo = { |
|
address: `${hostname}:${port}`, |
|
}; |
|
if (userCred) { |
|
result.creds = userCred; |
|
} |
|
trace( |
|
'Proxy server ' + result.address + ' set by environment variable ' + envVar |
|
); |
|
return result; |
|
} |
|
|
|
function getNoProxyHostList(): string[] { |
|
|
|
let noProxyStr: string | undefined = process.env.no_grpc_proxy; |
|
let envVar = 'no_grpc_proxy'; |
|
if (!noProxyStr) { |
|
noProxyStr = process.env.no_proxy; |
|
envVar = 'no_proxy'; |
|
} |
|
if (noProxyStr) { |
|
trace('No proxy server list set by environment variable ' + envVar); |
|
return noProxyStr.split(','); |
|
} else { |
|
return []; |
|
} |
|
} |
|
|
|
export interface ProxyMapResult { |
|
target: GrpcUri; |
|
extraOptions: ChannelOptions; |
|
} |
|
|
|
export function mapProxyName( |
|
target: GrpcUri, |
|
options: ChannelOptions |
|
): ProxyMapResult { |
|
const noProxyResult: ProxyMapResult = { |
|
target: target, |
|
extraOptions: {}, |
|
}; |
|
if ((options['grpc.enable_http_proxy'] ?? 1) === 0) { |
|
return noProxyResult; |
|
} |
|
if (target.scheme === 'unix') { |
|
return noProxyResult; |
|
} |
|
const proxyInfo = getProxyInfo(); |
|
if (!proxyInfo.address) { |
|
return noProxyResult; |
|
} |
|
const hostPort = splitHostPort(target.path); |
|
if (!hostPort) { |
|
return noProxyResult; |
|
} |
|
const serverHost = hostPort.host; |
|
for (const host of getNoProxyHostList()) { |
|
if (host === serverHost) { |
|
trace( |
|
'Not using proxy for target in no_proxy list: ' + uriToString(target) |
|
); |
|
return noProxyResult; |
|
} |
|
} |
|
const extraOptions: ChannelOptions = { |
|
'grpc.http_connect_target': uriToString(target), |
|
}; |
|
if (proxyInfo.creds) { |
|
extraOptions['grpc.http_connect_creds'] = proxyInfo.creds; |
|
} |
|
return { |
|
target: { |
|
scheme: 'dns', |
|
path: proxyInfo.address, |
|
}, |
|
extraOptions: extraOptions, |
|
}; |
|
} |
|
|
|
export interface ProxyConnectionResult { |
|
socket?: Socket; |
|
realTarget?: GrpcUri; |
|
} |
|
|
|
export function getProxiedConnection( |
|
address: SubchannelAddress, |
|
channelOptions: ChannelOptions, |
|
connectionOptions: tls.ConnectionOptions |
|
): Promise<ProxyConnectionResult> { |
|
if (!('grpc.http_connect_target' in channelOptions)) { |
|
return Promise.resolve<ProxyConnectionResult>({}); |
|
} |
|
const realTarget = channelOptions['grpc.http_connect_target'] as string; |
|
const parsedTarget = parseUri(realTarget); |
|
if (parsedTarget === null) { |
|
return Promise.resolve<ProxyConnectionResult>({}); |
|
} |
|
const splitHostPost = splitHostPort(parsedTarget.path); |
|
if (splitHostPost === null) { |
|
return Promise.resolve<ProxyConnectionResult>({}); |
|
} |
|
const hostPort = `${splitHostPost.host}:${ |
|
splitHostPost.port ?? DEFAULT_PORT |
|
}`; |
|
const options: http.RequestOptions = { |
|
method: 'CONNECT', |
|
path: hostPort, |
|
}; |
|
const headers: http.OutgoingHttpHeaders = { |
|
Host: hostPort, |
|
}; |
|
|
|
if (isTcpSubchannelAddress(address)) { |
|
options.host = address.host; |
|
options.port = address.port; |
|
} else { |
|
options.socketPath = address.path; |
|
} |
|
if ('grpc.http_connect_creds' in channelOptions) { |
|
headers['Proxy-Authorization'] = |
|
'Basic ' + |
|
Buffer.from(channelOptions['grpc.http_connect_creds'] as string).toString( |
|
'base64' |
|
); |
|
} |
|
options.headers = headers; |
|
const proxyAddressString = subchannelAddressToString(address); |
|
trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path); |
|
return new Promise<ProxyConnectionResult>((resolve, reject) => { |
|
const request = http.request(options); |
|
request.once('connect', (res, socket, head) => { |
|
request.removeAllListeners(); |
|
socket.removeAllListeners(); |
|
if (res.statusCode === 200) { |
|
trace( |
|
'Successfully connected to ' + |
|
options.path + |
|
' through proxy ' + |
|
proxyAddressString |
|
); |
|
if ('secureContext' in connectionOptions) { |
|
|
|
|
|
|
|
|
|
const targetPath = getDefaultAuthority(parsedTarget); |
|
const hostPort = splitHostPort(targetPath); |
|
const remoteHost = hostPort?.host ?? targetPath; |
|
|
|
const cts = tls.connect( |
|
{ |
|
host: remoteHost, |
|
servername: remoteHost, |
|
socket: socket, |
|
...connectionOptions, |
|
}, |
|
() => { |
|
trace( |
|
'Successfully established a TLS connection to ' + |
|
options.path + |
|
' through proxy ' + |
|
proxyAddressString |
|
); |
|
resolve({ socket: cts, realTarget: parsedTarget }); |
|
} |
|
); |
|
cts.on('error', (error: Error) => { |
|
trace( |
|
'Failed to establish a TLS connection to ' + |
|
options.path + |
|
' through proxy ' + |
|
proxyAddressString + |
|
' with error ' + |
|
error.message |
|
); |
|
reject(); |
|
}); |
|
} else { |
|
trace( |
|
'Successfully established a plaintext connection to ' + |
|
options.path + |
|
' through proxy ' + |
|
proxyAddressString |
|
); |
|
resolve({ |
|
socket, |
|
realTarget: parsedTarget, |
|
}); |
|
} |
|
} else { |
|
log( |
|
LogVerbosity.ERROR, |
|
'Failed to connect to ' + |
|
options.path + |
|
' through proxy ' + |
|
proxyAddressString + |
|
' with status ' + |
|
res.statusCode |
|
); |
|
reject(); |
|
} |
|
}); |
|
request.once('error', err => { |
|
request.removeAllListeners(); |
|
log( |
|
LogVerbosity.ERROR, |
|
'Failed to connect to proxy ' + |
|
proxyAddressString + |
|
' with error ' + |
|
err.message |
|
); |
|
reject(); |
|
}); |
|
request.end(); |
|
}); |
|
} |
|
|