Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APPS-47043] Allow user to provide a specific port and host for containerized environment #1004

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,855 changes: 949 additions & 906 deletions index.d.ts

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions lib/authentication/auth_web.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { rest } = require('../global_config');
function AuthWeb(connectionConfig, httpClient, webbrowser) {

const host = connectionConfig.host;
const samlRedirectUri = connectionConfig.getSamlRedirectUri();
const browserActionTimeout = connectionConfig.getBrowserActionTimeout();
const ssoUrlProvider = new SsoUrlProvider(httpClient);

Expand Down Expand Up @@ -50,6 +51,24 @@ function AuthWeb(connectionConfig, httpClient, webbrowser) {
body['data']['AUTHENTICATOR'] = 'EXTERNALBROWSER';
};

/**
* net's server.listen is async, so it isn't ready until after the 'listening' event is emitted
* @param {*} server
* @param {*} port
* @param {*} host
* @returns
*/
function listenAsync(server, port, host) {
return new Promise((resolve, reject) => {
// When custom parameters are not provided, the port will be set to 0.
// That means it will use a random port and fallback to localhost
server.listen(port, host);

server.on('listening', () => resolve(server.address()));
server.on('error', reject);
});
}

/**
* Obtain SAML token through SSO URL.
*
Expand All @@ -71,8 +90,12 @@ function AuthWeb(connectionConfig, httpClient, webbrowser) {
return result;
});

// Use a free random port and set to no backlog
server.listen(0, 0);
try {
const address = URLUtil.parseAddress(samlRedirectUri);
await listenAsync(server, address.port, address.host);
} catch (err) {
throw new Error(util.format('Error while creating server, samlRedirectUri likely incorrect: %s', err));
}

if (connectionConfig.getDisableConsoleLogin()) {
// Step 1: query Snowflake to obtain SSO url
Expand Down
16 changes: 16 additions & 0 deletions lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const DEFAULT_PARAMS =
'forceGCPUseDownscopedCredential',
'representNullAsStringNull',
'disableSamlURLCheck',
'samlRedirectUri',
'credentialCacheDir',
'passcodeInPassword',
'passcode',
Expand Down Expand Up @@ -476,6 +477,13 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
disableSamlURLCheck = options.disableSamlURLCheck;
}

let samlRedirectUri = 'localhost:0';
if (Util.exists(options.samlRedirectUri)) {
Errors.checkArgumentValid(Util.isString(options.samlRedirectUri),
ErrorCodes.ERR_CONN_CREATE_INVALID_SAML_REDIRECT_URI);
samlRedirectUri = options.samlRedirectUri;
}

let clientStoreTemporaryCredential = false;
if (Util.exists(options.clientStoreTemporaryCredential)) {
Errors.checkArgumentValid(Util.isBoolean(options.clientStoreTemporaryCredential),
Expand Down Expand Up @@ -807,6 +815,14 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
return disableSamlURLCheck;
};

/**
* Returns a custom address for the local server to redirect to after SAML authentication.
* @returns {String}
*/
this.getSamlRedirectUri = function () {
return samlRedirectUri;
};

this.getCredentialCacheDir = function () {
return credentialCacheDir;
};
Expand Down
1 change: 1 addition & 0 deletions lib/constants/error_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ exports[404053] = 'A host must be specified.';
exports[404054] = 'Invalid host. The specified value must be a string.';
exports[404055] = 'Invalid passcodeInPassword. The specified value must be a boolean';
exports[404056] = 'Invalid passcode. The specified value must be a string';
exports[404057] = 'Invalid samlRedirectUri. The specified value must be a string.';

// 405001
exports[405001] = 'Invalid callback. The specified value must be a function.';
Expand Down
1 change: 1 addition & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ codes.ERR_CONN_CREATE_MISSING_HOST = 404053;
codes.ERR_CONN_CREATE_INVALID_HOST = 404054;
codes.ERR_CONN_CREATE_INVALID_PASSCODE_IN_PASSWORD = 404055;
codes.ERR_CONN_CREATE_INVALID_PASSCODE = 404056;
codes.ERR_CONN_CREATE_INVALID_SAML_REDIRECT_URI = 404057;

// 405001
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;
Expand Down
36 changes: 36 additions & 0 deletions lib/url_util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Logger = require('./logger');
const net = require('net');

/**
* Determines if a given URL is valid.
Expand Down Expand Up @@ -27,4 +28,39 @@ exports.urlEncode = function (url) {
/** The encodeURIComponent() method encodes special characters including: , / ? : @ & = + $ #
but escapes space as %20B. Replace with + for consistency across drivers. */
return encodeURIComponent(url).replace(/%20/g, '+');
};

/**
* Returns an object with host and port properties.
* Unspecified ports get set to 0.
* @param {String} address
*
* @returns {Object} { host: string, port: number }
*/
exports.parseAddress = function (address) {
let host, port;

// upfront check for IPv6 before we take the port off
if (net.isIPv6(address)) {
host = address;
port = 0;
return { host, port };
}

const match = address.match(/^(.*):(\d+)$/);

if (match) {
host = match[1];
port = parseInt(match[2], 10);
} else {
host = address;
port = 0;
}

// Remove brackets from IPv6 addresses with ports
if (host.startsWith('[') && host.endsWith(']')) {
host = host.slice(1, -1);
}

return { host, port };
};
2 changes: 1 addition & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,4 +778,4 @@ exports.lstrip = function (str, remove) {
str = str.substr(1);
}
return str;
};
};
14 changes: 13 additions & 1 deletion test/unit/authentication/authentication_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const AuthOkta = require('./../../../lib/authentication/auth_okta');
const AuthIDToken = require('./../../../lib/authentication/auth_idtoken');
const AuthenticationTypes = require('./../../../lib/authentication/authentication_types');
const MockTestUtil = require('./../mock/mock_test_util');
const { getPortFree } = require('../test_util');

// get connection options to connect to this mock snowflake instance
const mockConnectionOptions = MockTestUtil.connectionOptions;
Expand Down Expand Up @@ -113,12 +114,14 @@ describe('external browser authentication', function () {

const credentials = connectionOptionsExternalBrowser;
const BROWSER_ACTION_TIMEOUT = 10000;

const connectionConfig = {
getBrowserActionTimeout: () => BROWSER_ACTION_TIMEOUT,
getProxy: () => {},
getAuthenticator: () => credentials.authenticator,
getServiceName: () => '',
getDisableConsoleLogin: () => true,
getSamlRedirectUri: () => '',
host: 'fakehost'
};

Expand Down Expand Up @@ -161,7 +164,13 @@ describe('external browser authentication', function () {
});

it('external browser - get success', async function () {
const auth = new AuthWeb(connectionConfig, httpclient, webbrowser.open);
const availablePort = await getPortFree();
const localConnectionConfig = {
...connectionConfig,
getSamlRedirectUri: () => `localhost:${availablePort}`
};

const auth = new AuthWeb(localConnectionConfig, httpclient, webbrowser.open);
await auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username);

const body = { data: {} };
Expand Down Expand Up @@ -199,13 +208,15 @@ describe('external browser authentication', function () {

webbrowser = require('webbrowser');
httpclient = require('httpclient');
const availablePort = await getPortFree();

const fastFailConnectionConfig = {
getBrowserActionTimeout: () => 10,
getProxy: () => {},
getAuthenticator: () => credentials.authenticator,
getServiceName: () => '',
getDisableConsoleLogin: () => true,
getSamlRedirectUri: () => `localhost:${availablePort}`,
host: 'fakehost'
};

Expand Down Expand Up @@ -694,6 +705,7 @@ describe('okta authentication', function () {
getClientStoreTemporaryCredential: () => true,
getPasscode: () => '',
getPasscodeInPassword: () => false,
getSamlRedirectUri: () => '127.0.0.1:8080',
idToken: idToken || null,
host: 'host',
};
Expand Down
20 changes: 20 additions & 0 deletions test/unit/connection/connection_config_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,17 @@ describe('ConnectionConfig: basic', function () {
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PASSCODE
},
{
name: 'invalid samlRedirectUri',

options: {
account: 'account',
username: 'username',
authenticator: 'EXTERNALBROWSER',
samlRedirectUri: 666666
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_SAML_REDIRECT_URI
},
];

const createNegativeITCallback = function (testCase) {
Expand Down Expand Up @@ -1662,6 +1673,15 @@ describe('ConnectionConfig: basic', function () {
result: '123456',
getter: 'getPasscode',
},
{
name: 'samlRedirectUri',
input: {
...mandatoryOption,
samlRedirectUri: 'localhost:3000',
},
result: 'localhost:3000',
getter: 'getSamlRedirectUri',
}
];

testCases.forEach(({ name, input, result, getter }) => {
Expand Down
5 changes: 3 additions & 2 deletions test/unit/mock/mock_test_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const connectionOptionsExternalBrowser =
accessUrl: 'http://fakeaccount.snowflakecomputing.com',
username: 'fakeusername',
account: 'fakeaccount',
authenticator: 'EXTERNALBROWSER'
authenticator: 'EXTERNALBROWSER',
};

const connectionOptionsidToken =
Expand Down Expand Up @@ -161,7 +161,8 @@ const connectionOptionsOkta =
getTimeout: () => 90,
getRetryTimeout: () => 300,
getRetrySfMaxLoginRetries: () => 7,
getDisableSamlURLCheck: () => false
getDisableSamlURLCheck: () => false,
getSamlRedirectUri: () => ''
};

exports.connectionOptions =
Expand Down
12 changes: 12 additions & 0 deletions test/unit/test_util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
const net = require('net');

module.exports.sleepAsync = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
};

module.exports.getPortFree = function () {
return new Promise(res => {
const srv = net.createServer();
srv.listen(0, () => {
const port = srv.address().port;
srv.close(() => res(port));
});
});
};
100 changes: 100 additions & 0 deletions test/unit/url_util_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,104 @@ describe('URLUtil', function () {
assert.equal(URLUtil.urlEncode('Test//String'), 'Test%2F%2FString');
assert.equal(URLUtil.urlEncode('Test+Plus'), 'Test%2BPlus');
});
});


describe('parseAddress function test', function () {
const testCases = [
{
name: 'test - when the address is localhost with a port',
address: 'localhost:4433',
result: {
host: 'localhost',
port: 4433
}
},
{
name: 'test - when the address is an ip address with a port',
address: '52.194.1.73:8080',
result: {
host: '52.194.1.73',
port: 8080
}
},
{
name: 'test - ipv4 address with no port',
address: '52.194.1.73',
result: {
host: '52.194.1.73',
port: 0
}
},
{
name: 'test - ipv6 address without brackets with no port',
address: '2001:db8:85a3:8d3:1319:8a2e:370:7348',
result: {
host: '2001:db8:85a3:8d3:1319:8a2e:370:7348',
port: 0
}
},
{
name: 'test - ipv6 address with no port',
address: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
result: {
host: '2001:db8:85a3:8d3:1319:8a2e:370:7348',
port: 0
}
},
{
name: 'test - ipv6 address with brackets with port',
address: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080',
result: {
host: '2001:db8:85a3:8d3:1319:8a2e:370:7348',
port: 8080
}
},
{
name: 'test - ipv6 address without brackets with port',
address: '2001:db8:85a3:8d3:1319:8a2e:370:7348:8080',
result: {
host: '2001:db8:85a3:8d3:1319:8a2e:370:7348',
port: 8080
}
},
{
name: 'test - ipv6 address abbreviated with no port',
address: 'fe00:0:0:1::92',
result: {
host: 'fe00:0:0:1::92',
port: 0
}
},
{
name: 'test - ipv6 address abbreviated with brackets with port',
address: '[fe00:0:0:1::92]:8080',
result: {
host: 'fe00:0:0:1::92',
port: 8080
}
},
{
name: 'test - user better not do this because it is wrong: ipv6 address abbreviated without brackets with port',
address: 'fe00:0:0:1::92:8080',
result: {
host: 'fe00:0:0:1::92:8080',
port: 0
}
},
{
name: 'test - hostname with port',
address: 'example.com:8080',
result: {
host: 'example.com',
port: 8080
}
}
];

for (const { name, address, result } of testCases) {
it(name, function () {
assert.deepStrictEqual(URLUtil.parseAddress(address), result);
});
}
});
Loading
Loading