Skip to content

Commit fd36b2e

Browse files
[PECO-1042] Add proxy support (#193)
* Introduce client context Signed-off-by: Levko Kravets <[email protected]> * Pass context to all relevant classes Signed-off-by: Levko Kravets <[email protected]> * Make driver a part of context Signed-off-by: Levko Kravets <[email protected]> * Fix tests Signed-off-by: Levko Kravets <[email protected]> * [PECO-1042] Add proxy support Signed-off-by: Levko Kravets <[email protected]> * Tidy up code a bit Signed-off-by: Levko Kravets <[email protected]> * Tidy up code a bit Signed-off-by: Levko Kravets <[email protected]> --------- Signed-off-by: Levko Kravets <[email protected]>
1 parent 9eb3807 commit fd36b2e

File tree

12 files changed

+621
-54
lines changed

12 files changed

+621
-54
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Release History
22

3+
## 1.x (unreleased)
4+
5+
### Highlights
6+
7+
- Proxy support added
8+
9+
### Proxy support
10+
11+
This feature allows to pass through proxy all the requests library makes. By default, proxy is disabled.
12+
To enable proxy, pass a configuration object to `DBSQLClient.connect`:
13+
14+
```ts
15+
client.connect({
16+
// pass host, path, auth options as usual
17+
proxy: {
18+
protocol: 'http', // supported protocols: 'http', 'https', 'socks', 'socks4', 'socks4a', 'socks5', 'socks5h'
19+
host: 'localhost', // proxy host (string)
20+
port: 8070, // proxy port (number)
21+
auth: { // optional proxy basic auth config
22+
username: ...
23+
password: ...
24+
},
25+
},
26+
})
27+
```
28+
329
## 1.5.0
430

531
### Highlights

lib/DBSQLClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
7373
path: prependSlash(options.path),
7474
https: true,
7575
socketTimeout: options.socketTimeout,
76+
proxy: options.proxy,
7677
headers: {
7778
'User-Agent': buildUserAgentString(options.clientId),
7879
},

lib/DBSQLSession.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,11 @@ export default class DBSQLSession implements IDBSQLSession {
274274
if (localFile === undefined) {
275275
throw new StagingError('Local file path not provided');
276276
}
277-
const response = await fetch(presignedUrl, { method: 'GET', headers });
277+
278+
const connectionProvider = await this.context.getConnectionProvider();
279+
const agent = await connectionProvider.getAgent();
280+
281+
const response = await fetch(presignedUrl, { method: 'GET', headers, agent });
278282
if (!response.ok) {
279283
throw new StagingError(`HTTP error ${response.status} ${response.statusText}`);
280284
}
@@ -283,7 +287,10 @@ export default class DBSQLSession implements IDBSQLSession {
283287
}
284288

285289
private async handleStagingRemove(presignedUrl: string, headers: HeadersInit): Promise<void> {
286-
const response = await fetch(presignedUrl, { method: 'DELETE', headers });
290+
const connectionProvider = await this.context.getConnectionProvider();
291+
const agent = await connectionProvider.getAgent();
292+
293+
const response = await fetch(presignedUrl, { method: 'DELETE', headers, agent });
287294
if (!response.ok) {
288295
throw new StagingError(`HTTP error ${response.status} ${response.statusText}`);
289296
}
@@ -297,8 +304,12 @@ export default class DBSQLSession implements IDBSQLSession {
297304
if (localFile === undefined) {
298305
throw new StagingError('Local file path not provided');
299306
}
307+
308+
const connectionProvider = await this.context.getConnectionProvider();
309+
const agent = await connectionProvider.getAgent();
310+
300311
const data = fs.readFileSync(localFile);
301-
const response = await fetch(presignedUrl, { method: 'PUT', headers, body: data });
312+
const response = await fetch(presignedUrl, { method: 'PUT', headers, agent, body: data });
302313
if (!response.ok) {
303314
throw new StagingError(`HTTP error ${response.status} ${response.statusText}`);
304315
}

lib/connection/auth/DatabricksOAuth/OAuthManager.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Issuer, BaseClient } from 'openid-client';
1+
import http from 'http';
2+
import { Issuer, BaseClient, custom } from 'openid-client';
23
import HiveDriverError from '../../../errors/HiveDriverError';
34
import { LogLevel } from '../../../contracts/IDBSQLLogger';
45
import OAuthToken from './OAuthToken';
@@ -26,6 +27,8 @@ export default abstract class OAuthManager {
2627

2728
protected readonly options: OAuthManagerOptions;
2829

30+
protected agent?: http.Agent;
31+
2932
protected issuer?: Issuer;
3033

3134
protected client?: BaseClient;
@@ -48,14 +51,35 @@ export default abstract class OAuthManager {
4851
}
4952

5053
protected async getClient(): Promise<BaseClient> {
54+
// Obtain http agent each time when we need an OAuth client
55+
// to ensure that we always use a valid agent instance
56+
const connectionProvider = await this.context.getConnectionProvider();
57+
this.agent = await connectionProvider.getAgent();
58+
59+
const getHttpOptions = () => ({
60+
agent: this.agent,
61+
});
62+
5163
if (!this.issuer) {
52-
const issuer = await Issuer.discover(this.getOIDCConfigUrl());
64+
// To use custom http agent in Issuer.discover(), we'd have to set Issuer[custom.http_options].
65+
// However, that's a static field, and if multiple instances of OAuthManager used, race condition
66+
// may occur when they simultaneously override that field and then try to use Issuer.discover().
67+
// Therefore we create a local class derived from Issuer, and set that field for it, thus making
68+
// sure that it will not interfere with other instances (or other code that may use Issuer)
69+
class CustomIssuer extends Issuer {
70+
static [custom.http_options] = getHttpOptions;
71+
}
72+
73+
const issuer = await CustomIssuer.discover(this.getOIDCConfigUrl());
74+
5375
// Overwrite `authorization_endpoint` in default config (specifically needed for Azure flow
5476
// where this URL has to be different)
5577
this.issuer = new Issuer({
5678
...issuer.metadata,
5779
authorization_endpoint: this.getAuthorizationUrl(),
5880
});
81+
82+
this.issuer[custom.http_options] = getHttpOptions;
5983
}
6084

6185
if (!this.client) {
@@ -64,6 +88,8 @@ export default abstract class OAuthManager {
6488
client_secret: this.options.clientSecret,
6589
token_endpoint_auth_method: this.options.clientSecret === undefined ? 'none' : 'client_secret_basic',
6690
});
91+
92+
this.client[custom.http_options] = getHttpOptions;
6793
}
6894

6995
return this.client;

lib/connection/connections/HttpConnection.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import thrift from 'thrift';
22
import https from 'https';
33
import http from 'http';
44
import { HeadersInit } from 'node-fetch';
5+
import { ProxyAgent } from 'proxy-agent';
56

67
import IConnectionProvider from '../contracts/IConnectionProvider';
7-
import IConnectionOptions from '../contracts/IConnectionOptions';
8+
import IConnectionOptions, { ProxyOptions } from '../contracts/IConnectionOptions';
89
import globalConfig from '../../globalConfig';
910

1011
import ThriftHttpConnection from './ThriftHttpConnection';
@@ -16,6 +17,8 @@ export default class HttpConnection implements IConnectionProvider {
1617

1718
private connection?: ThriftHttpConnection;
1819

20+
private agent?: http.Agent;
21+
1922
constructor(options: IConnectionOptions) {
2023
this.options = options;
2124
}
@@ -28,26 +31,59 @@ export default class HttpConnection implements IConnectionProvider {
2831
});
2932
}
3033

31-
private async getAgent(): Promise<http.Agent> {
32-
const { options } = this;
34+
public async getAgent(): Promise<http.Agent> {
35+
if (!this.agent) {
36+
if (this.options.proxy !== undefined) {
37+
this.agent = this.createProxyAgent(this.options.proxy);
38+
} else {
39+
this.agent = this.options.https ? this.createHttpsAgent() : this.createHttpAgent();
40+
}
41+
}
42+
43+
return this.agent;
44+
}
3345

34-
const httpAgentOptions: http.AgentOptions = {
46+
private getAgentDefaultOptions(): http.AgentOptions {
47+
return {
3548
keepAlive: true,
3649
maxSockets: 5,
3750
keepAliveMsecs: 10000,
38-
timeout: options.socketTimeout ?? globalConfig.socketTimeout,
51+
timeout: this.options.socketTimeout ?? globalConfig.socketTimeout,
3952
};
53+
}
4054

55+
private createHttpAgent(): http.Agent {
56+
const httpAgentOptions = this.getAgentDefaultOptions();
57+
return new http.Agent(httpAgentOptions);
58+
}
59+
60+
private createHttpsAgent(): https.Agent {
4161
const httpsAgentOptions: https.AgentOptions = {
42-
...httpAgentOptions,
62+
...this.getAgentDefaultOptions(),
4363
minVersion: 'TLSv1.2',
4464
rejectUnauthorized: false,
45-
ca: options.ca,
46-
cert: options.cert,
47-
key: options.key,
65+
ca: this.options.ca,
66+
cert: this.options.cert,
67+
key: this.options.key,
4868
};
69+
return new https.Agent(httpsAgentOptions);
70+
}
71+
72+
private createProxyAgent(proxyOptions: ProxyOptions): ProxyAgent {
73+
const proxyAuth = proxyOptions.auth?.username
74+
? `${proxyOptions.auth.username}:${proxyOptions.auth?.password ?? ''}@`
75+
: '';
76+
const proxyUrl = `${proxyOptions.protocol}://${proxyAuth}${proxyOptions.host}:${proxyOptions.port}`;
4977

50-
return options.https ? new https.Agent(httpsAgentOptions) : new http.Agent(httpAgentOptions);
78+
const proxyProtocol = `${proxyOptions.protocol}:`;
79+
80+
return new ProxyAgent({
81+
...this.getAgentDefaultOptions(),
82+
getProxyForUrl: () => proxyUrl,
83+
httpsAgent: this.createHttpsAgent(),
84+
httpAgent: this.createHttpAgent(),
85+
protocol: proxyProtocol,
86+
});
5187
}
5288

5389
public async getThriftConnection(): Promise<any> {

lib/connection/contracts/IConnectionOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { HeadersInit } from 'node-fetch';
22

3+
export interface ProxyOptions {
4+
protocol: 'http' | 'https' | 'socks' | 'socks4' | 'socks4a' | 'socks5' | 'socks5h';
5+
host: string;
6+
port: number;
7+
auth?: {
8+
username?: string;
9+
password?: string;
10+
};
11+
}
12+
313
export default interface IConnectionOptions {
414
host: string;
515
port: number;
616
path?: string;
717
https?: boolean;
818
headers?: HeadersInit;
919
socketTimeout?: number;
20+
proxy?: ProxyOptions;
1021

1122
ca?: Buffer | string;
1223
cert?: Buffer | string;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import http from 'http';
12
import { HeadersInit } from 'node-fetch';
23

34
export default interface IConnectionProvider {
45
getThriftConnection(): Promise<any>;
56

7+
getAgent(): Promise<http.Agent>;
8+
69
setHeaders(headers: HeadersInit): void;
710
}

lib/contracts/IDBSQLClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import IDBSQLLogger from './IDBSQLLogger';
22
import IDBSQLSession from './IDBSQLSession';
33
import IAuthentication from '../connection/contracts/IAuthentication';
4+
import { ProxyOptions } from '../connection/contracts/IConnectionOptions';
45
import OAuthPersistence from '../connection/auth/DatabricksOAuth/OAuthPersistence';
56

67
export interface ClientOptions {
@@ -30,6 +31,7 @@ export type ConnectionOptions = {
3031
path: string;
3132
clientId?: string;
3233
socketTimeout?: number;
34+
proxy?: ProxyOptions;
3335
} & AuthOptions;
3436

3537
export interface OpenSessionRequest {

lib/result/CloudFetchResult.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export default class CloudFetchResult extends ArrowResult {
5252
}
5353

5454
private async fetch(url: RequestInfo, init?: RequestInit) {
55-
return fetch(url, init);
55+
const connectionProvider = await this.context.getConnectionProvider();
56+
const agent = await connectionProvider.getAgent();
57+
58+
return fetch(url, {
59+
agent,
60+
...init,
61+
});
5662
}
5763
}

0 commit comments

Comments
 (0)