Skip to content

Commit

Permalink
Upgrade https-proxy-agent
Browse files Browse the repository at this point in the history
Add env vars for proxy options

Upgrade https-proxy-agent - PR-679 - add protocol to connectionConfig proxy
  • Loading branch information
sfc-gh-pmotacki committed Dec 5, 2023
1 parent b01683e commit 4cb67ea
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 286 deletions.
292 changes: 20 additions & 272 deletions lib/agent/https_proxy_ocsp_agent.js
Original file line number Diff line number Diff line change
@@ -1,285 +1,33 @@
/*
* Copyright (c) 2013 Nathan Rajlich <[email protected]>
* Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved.
*/

/**
* Module dependencies.
*/

var net = require('net');
var tls = require('tls');
var url = require('url');
var extend = require('extend');
var HttpsProxyAgent = require('https-proxy-agent');
var createAgent = require('agent-base');
var inherits = require('util').inherits;
var debug = require('debug')('https-proxy-agent');
var SocketUtil = require('./socket_util');
const tls = require('tls');
const { HttpsProxyAgent } = require('https-proxy-agent');
const SocketUtil = require('./socket_util');
const Logger = require('../logger');

/**
* Module exports.
*/

module.exports = HttpsProxyOcspAgent;

/**
* The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to the
* specified "HTTP(s) proxy server" in order to proxy HTTPS requests.
*
* @api public
*/

function HttpsProxyOcspAgent(opts)
{
if (!(this instanceof HttpsProxyOcspAgent))
{
return new HttpsProxyOcspAgent(opts);
}
if ('string' == typeof opts)
{
opts = url.parse(opts);
}
if (!opts)
{
throw new Error('an HTTP(S) proxy server `host` and `port` must be specified!');
}
debug('creating new HttpsProxyAgent instance: %o', opts);
var HttpsAgent = new HttpsProxyAgent(opts);
var Agent = createAgent.call(this, connect);
HttpsAgent.callback = Agent.callback;
HttpsAgent.timeout = Agent.timeout;
HttpsAgent.options = Agent.opts;

var proxy = extend({}, opts);

// if `true`, then connect to the proxy server over TLS. defaults to `false`.
HttpsAgent.secureProxy = proxy.protocol ? /^https:?$/i.test(proxy.protocol) : false;

// prefer `hostname` over `host`, and set the `port` if needed
proxy.host = proxy.hostname || proxy.host;
proxy.port = +proxy.port || (this.secureProxy ? 443 : 80);

if (proxy.host && proxy.path)
{
// if both a `host` and `path` are specified then it's most likely the
// result of a `url.parse()` call... we need to remove the `path` portion so
// that `net.connect()` doesn't attempt to open that as a unix socket file.
delete proxy.path;
delete proxy.pathname;
}

if (proxy.user && proxy.password)
{
// user:password
proxy.auth = proxy.user + ':' + proxy.password;
delete proxy.user;
delete proxy.password;

if (!HttpsAgent.secureProxy)
{
Logger.getInstance().warn("Warning: connecting to an authenticated proxy server through HTTP. To use HTTPS, set 'proxyProtocol' to 'HTTPS'")
}
}
module.exports = createHttpsProxyAgent;

HttpsAgent.proxy = proxy;
return HttpsAgent;
function createHttpsProxyAgent(opts) {
// HttpsProxyAgent >= 6.x takes two arguments for its constructor
// See: https://github.com/TooTallNate/proxy-agents/blob/main/packages/https-proxy-agent/CHANGELOG.md#600
const { host: hostname, port, user: username, password, protocol: rawProtocol, ...agentOptions } = opts;
const protocol = rawProtocol.endsWith(':') ? rawProtocol : `${rawProtocol}:`;
return new HttpsProxyOcspAgent({ hostname, port, username, password, protocol }, agentOptions);

Check warning on line 17 in lib/agent/https_proxy_ocsp_agent.js

View check run for this annotation

Codecov / codecov/patch

lib/agent/https_proxy_ocsp_agent.js#L15-L17

Added lines #L15 - L17 were not covered by tests
}

inherits(HttpsProxyAgent, createAgent);

/**
* Called when the node-core HTTP client library is creating a new HTTP request.
*
* @api public
*/

function connect(req, opts, fn)
{
var proxy = this.proxy;
var agent = this;

// is proxy same as the host it's proxying for? that's a problem
// can occur when envvar HTTPS_PROXY is the same as Connection proxyHost
if (proxy.host === opts.host) {
Logger.getInstance().warn('Looks like the proxy (%s) is the same as the host it is proxying for (%s). '
+ 'If you have connectivity problems, please check if HTTPS_PROXY and proxyHost:proxyPort '
+ 'settings are both in effect and if so, try unsetting one of them.', proxy.host, opts.host);
};

// create a socket connection to the proxy server
var socket;
if (this.secureProxy)
{
socket = tls.connect(proxy);
}
else
{
socket = net.connect(proxy);
}

// we need to buffer any HTTP traffic that happens with the proxy before we get
// the CONNECT response, so that if the response is anything other than an "200"
// response code, then we can re-play the "data" events on the socket once the
// HTTP parser is hooked up...
var buffers = [];
var buffersLength = 0;

function read()
{
var b = socket.read();
if (b)
{
ondata(b);
}
else
{
socket.once('readable', read);
}
}

function cleanup()
{
socket.removeListener('data', ondata);
socket.removeListener('end', onend);
socket.removeListener('error', onerror);
socket.removeListener('close', onclose);
socket.removeListener('readable', read);
}

function onclose(err)
{
debug('onclose had error %o', err);
}

function onend()
{
debug('onend');
}

function onerror(err)
{
cleanup();
fn(err);
}

function ondata(b)
{
buffers.push(b);
buffersLength += b.length;
var buffered = Buffer.concat(buffers, buffersLength);
var str = buffered.toString('ascii');

if (!~str.indexOf('\r\n\r\n'))
{
// keep buffering
debug('have not received end of HTTP headers yet...');
if (socket.read)
{
read();
}
else
{
socket.once('data', ondata);
}
return;
}

var firstLine = str.substring(0, str.indexOf('\r\n'));
var statusCode = +firstLine.split(' ')[1];
debug('got proxy server response: %o', firstLine);

if (200 === statusCode)
{
// 200 Connected status code!
var sock = socket;

// nullify the buffered data since we won't be needing it
buffers = buffered = null;

if (opts.secureEndpoint)
{
var host = opts.host;
// since the proxy is connecting to an SSL server, we have
// to upgrade this socket connection to an SSL connection
debug('upgrading proxy-connected socket to TLS connection: %o', opts.host);
opts.socket = socket;
opts.servername = opts.host;
opts.host = null;
opts.hostname = null;
opts.port = null;
sock = tls.connect(opts);
// pass in proxy agent to apply proxy for ocsp connection
// ocsp connection won't be secureEndpoint so no worry for recursive
// ocsp validation
SocketUtil.secureSocket(sock, host, agent);
}

cleanup();
fn(null, sock);
}
else
{
// some other status code that's not 200... need to re-play the HTTP header
// "data" events onto the socket once the HTTP machinery is attached so that
// the user can parse and handle the error status code
cleanup();

// save a reference to the concat'd Buffer for the `onsocket` callback
buffers = buffered;

// need to wait for the "socket" event to re-play the "data" events
req.once('socket', onsocket);
fn(null, socket);
}
class HttpsProxyOcspAgent extends HttpsProxyAgent {
constructor(proxy, opts) {
super(proxy, opts);

Check warning on line 22 in lib/agent/https_proxy_ocsp_agent.js

View check run for this annotation

Codecov / codecov/patch

lib/agent/https_proxy_ocsp_agent.js#L22

Added line #L22 was not covered by tests
}

function onsocket(socket)
{
// replay the "buffers" Buffer onto the `socket`, since at this point
// the HTTP module machinery has been hooked up for the user
if ('function' == typeof socket.ondata)
{
// node <= v0.11.3, the `ondata` function is set on the socket
socket.ondata(buffers, 0, buffers.length);
}
else if (socket.listeners('data').length > 0)
{
// node > v0.11.3, the "data" event is listened for directly
socket.emit('data', buffers);
}
else
{
// never?
throw new Error('should not happen...');
async connect(req, opts) {
Logger.getInstance().debug('Using proxy=%s for host %s', this.proxy.hostname, opts.host);
const socket = await super.connect(req, opts);
if (socket instanceof tls.TLSSocket) {
SocketUtil.secureSocket(socket, opts.host, this);

Check warning on line 29 in lib/agent/https_proxy_ocsp_agent.js

View check run for this annotation

Codecov / codecov/patch

lib/agent/https_proxy_ocsp_agent.js#L26-L29

Added lines #L26 - L29 were not covered by tests
}

// nullify the cached Buffer instance
buffers = null;
}

socket.on('error', onerror);
socket.on('close', onclose);
socket.on('end', onend);

if (socket.read)
{
read();
}
else
{
socket.once('data', ondata);
}

var hostname = opts.host + ':' + opts.port;
var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n';
var auth = proxy.auth;
if (auth)
{
msg += 'Proxy-Authorization: Basic ' + Buffer.from(auth, 'utf8').toString('base64') + '\r\n';
return socket;

Check warning on line 31 in lib/agent/https_proxy_ocsp_agent.js

View check run for this annotation

Codecov / codecov/patch

lib/agent/https_proxy_ocsp_agent.js#L31

Added line #L31 was not covered by tests
}
msg += 'Host: ' + hostname + '\r\n' +
'Connection: close\r\n' +
'\r\n';
socket.write(msg);
}
3 changes: 2 additions & 1 deletion lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
var proxyPort = options.proxyPort;
var proxyUser = options.proxyUser;
var proxyPassword = options.proxyPassword;
var proxyProtocol = options.proxyProtocol;
var proxyProtocol = options.proxyProtocol || 'http';
var noProxy = options.noProxy;

// if we're running in node and some proxy information is specified
Expand Down Expand Up @@ -267,6 +267,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
{
host: proxyHost,
port: proxyPort,
protocol: proxyProtocol,
noProxy: noProxy
};
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"fastest-levenshtein": "^1.0.16",
"generic-pool": "^3.8.2",
"glob": "^9.0.0",
"https-proxy-agent": "^5.0.1",
"https-proxy-agent": "^7.0.2",
"jsonwebtoken": "^9.0.0",
"mime-types": "^2.1.29",
"mkdirp": "^1.0.3",
Expand Down
25 changes: 13 additions & 12 deletions system_test/connectionOptions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved.
*/
var externalAccount =
const externalAccount =
{
accessUrl: 'http://externalaccount.reg.local.snowflakecomputing.com:8082',
username: 'snowman',
Expand All @@ -14,6 +14,9 @@ let snowflakeTestHost = process.env.SNOWFLAKE_TEST_HOST;
let snowflakeTestPort = process.env.SNOWFLAKE_TEST_PORT;
let snowflakeTestProxyHost = process.env.SNOWFLAKE_TEST_PROXY_HOST;
let snowflakeTestProxyPort = process.env.SNOWFLAKE_TEST_PROXY_PORT;
const snowflakeTestProxyProtocol = process.env.SNOWFLAKE_TEST_PROXY_PROTOCOL;
const snowflakeTestProxyUser = process.env.SNOWFLAKE_TEST_PROXY_USER;
const snowflakeTestProxyPassword = process.env.SNOWFLAKE_TEST_PROXY_PASSWORD;
const snowflakeTestAccount = process.env.SNOWFLAKE_TEST_ACCOUNT;
const snowflakeTestUser = process.env.SNOWFLAKE_TEST_USER;
const snowflakeTestDatabase = process.env.SNOWFLAKE_TEST_DATABASE;
Expand All @@ -22,28 +25,23 @@ const snowflakeTestSchema = process.env.SNOWFLAKE_TEST_SCHEMA;
const snowflakeTestRole = process.env.SNOWFLAKE_TEST_ROLE;
const snowflakeTestPassword = process.env.SNOWFLAKE_TEST_PASSWORD;

if (snowflakeTestProtocol === undefined)
{
if (snowflakeTestProtocol === undefined) {
snowflakeTestProtocol = 'https';
}

if (snowflakeTestHost === undefined)
{
if (snowflakeTestHost === undefined) {
snowflakeTestHost = snowflakeTestAccount + '.snowflakecomputing.com';
}

if (snowflakeTestPort === undefined)
{
if (snowflakeTestPort === undefined) {
snowflakeTestPort = '443';
}

if (snowflakeTestProxyHost === undefined)
{
if (snowflakeTestProxyHost === undefined) {
snowflakeTestProxyHost = 'localhost';
}

if (snowflakeTestProxyPort === undefined)
{
if (snowflakeTestProxyPort === undefined) {
snowflakeTestProxyPort = '3128';
}

Expand All @@ -61,7 +59,10 @@ const connectionWithProxy =
schema: snowflakeTestSchema,
role: snowflakeTestRole,
proxyHost: snowflakeTestProxyHost,
proxyPort: parseInt(snowflakeTestProxyPort, 10)
proxyPort: parseInt(snowflakeTestProxyPort, 10),
proxyProtocol: snowflakeTestProxyProtocol,
proxyUser: snowflakeTestProxyUser,
proxyPassword: snowflakeTestProxyPassword,
};

exports.externalAccount = externalAccount;
Expand Down

0 comments on commit 4cb67ea

Please sign in to comment.