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

Adds the ability to use a proxy, Closes #2698 #4679

Closed
wants to merge 17 commits into from
17 changes: 17 additions & 0 deletions docs/docs/user-guide/using-proxy-url.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: Use proxy url
sidebar_position: 15
---

# Configure CLI for Microsoft 365 to use a proxy

If you are behind a corporate proxy, you'll be able to use the CLI for Microsoft 365 when the environment variable `HTTP_PROXY` or `HTTPS_PROXY` is set.

## Understanding Proxy URL

When using a proxy, it's important to understand the different parts of a proxy URL. A proxy URL typically consists of the following elements:

- **protocol**: The protocol used by the proxy server, such as `HTTP`, `HTTPS`, or `SOCKS`
- **username and password**: if the proxy server requires authentication, you will need to provide a username and password
- **host**: the hostname or IP address of the proxy server
- **port number**: the port number on which the proxy server is listening. Defaults to 443 for the `HTTPS` protocol, otherwise it defaults to 80 when not provided
3 changes: 2 additions & 1 deletion src/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ export class Auth {
},
piiLoggingEnabled: false,
logLevel: debug ? LogLevel.Verbose : LogLevel.Error
}
},
proxyUrl: process.env.HTTP_PROXY || process.env.HTTPS_PROXY
}
};
}
Expand Down
16 changes: 16 additions & 0 deletions src/appInsights.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,20 @@ describe('appInsights', () => {
const i: any = await import(`./appInsights.js#${Math.random()}`);
assert(i.default.commonProperties.env === 'docker');
});

it('sets proxyHttpUrl in the telemetry', async () => {
const proxyHttpUrl = 'http://username:password@proxy.contoso.com:8080';
sinon.stub(process, 'env').value({ 'HTTP_PROXY': proxyHttpUrl });

const i: any = await import(`./appInsights.js#${Math.random()}`);
assert(i.default.config.proxyHttpUrl === proxyHttpUrl);
});

it('sets proxyHttpsUrl in the telemetry', async () => {
const proxyHttpsUrl = 'https://username:password@proxy.contoso.com:8080';
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': proxyHttpsUrl });

const i: any = await import(`./appInsights.js#${Math.random()}`);
assert(i.default.config.proxyHttpsUrl === proxyHttpsUrl);
});
});
3 changes: 3 additions & 0 deletions src/appInsights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ appInsightsClient.commonProperties = {
ci: Boolean(process.env.CI).toString()
};

appInsightsClient.config.proxyHttpUrl = process.env.HTTP_PROXY ?? '';
appInsightsClient.config.proxyHttpsUrl = process.env.HTTPS_PROXY ?? '';

appInsightsClient.context.tags['ai.cloud.roleInstance'] = crypto.createHash('sha256').update(appInsightsClient.context.tags['ai.cloud.roleInstance']).digest('hex');
delete appInsightsClient.context.tags['ai.cloud.role'];
delete appInsightsClient.context.tags['ai.cloud.roleName'];
Expand Down
101 changes: 101 additions & 0 deletions src/request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Request', () => {
afterEach(() => {
_request.debug = false;
sinonUtil.restore([
process.env,
global.setTimeout,
https.request,
(_request as any).req,
Expand Down Expand Up @@ -394,6 +395,106 @@ describe('Request', () => {
});
});

it('returns response of a successful GET request, with a proxy url', (done) => {
nicodecleyre marked this conversation as resolved.
Show resolved Hide resolved
let outcome = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be clearer to name the variable after what it represents, eg. proxyConfigured

sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080' });

sinon.stub(_request as any, 'req').callsFake((options) => {
_options = options as CliRequestOptions;
if (_options.proxy && _options.proxy.host === 'proxy.contoso.com' && _options.proxy.port === 8080 && _options.proxy.protocol === 'http') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this code by assigning the outcome of the comparison directly to the proxyConfigured variable without an if

outcome = true;
}
return Promise.resolve({ data: {} });
});

_request
.get({
url: 'https://contoso.sharepoint.com/'
})
.then(() => {
assert(outcome);
done();
}, (err) => {
done(err);
});
});

it('returns response of a successful GET request, with a proxy url and defaults port to 80', (done) => {
let outcome = false;
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com' });

sinon.stub(_request as any, 'req').callsFake((options) => {
_options = options as CliRequestOptions;
if (_options.proxy && _options.proxy.host === 'proxy.contoso.com' && _options.proxy.port === 80 && _options.proxy.protocol === 'http') {
outcome = true;
}
return Promise.resolve({ data: {} });
});

_request
.get({
url: 'https://contoso.sharepoint.com/'
})
.then(() => {
assert(outcome);
done();
}, (err) => {
done(err);
});
});

it('returns response of a successful GET request, with a proxy url and defaults port to 443', (done) => {
let outcome = false;
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'https://proxy.contoso.com' });

sinon.stub(_request as any, 'req').callsFake((options) => {
_options = options as CliRequestOptions;
if (_options.proxy && _options.proxy.host === 'proxy.contoso.com' && _options.proxy.port === 443 && _options.proxy.protocol === 'http') {
outcome = true;
}
return Promise.resolve({ data: {} });
});

_request
.get({
url: 'https://contoso.sharepoint.com/'
})
.then(() => {
assert(outcome);
done();
}, (err) => {
done(err);
});
});

it('returns response of a successful GET request, with a proxy url with username and password', (done) => {
let outcome = false;
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://username:password@proxy.contoso.com:8080' });

sinon.stub(_request as any, 'req').callsFake((options) => {
_options = options as CliRequestOptions;
if (_options.proxy && _options.proxy.host === 'proxy.contoso.com'
&& _options.proxy.port === 8080
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put operators at the end of line for readability

&& _options.proxy.protocol === 'http'
&& _options.proxy.auth?.username === 'username'
&& _options.proxy.auth?.password === 'password') {
outcome = true;
}
return Promise.resolve({ data: {} });
});

_request
.get({
url: 'https://contoso.sharepoint.com/'
})
.then(() => {
assert(outcome);
done();
}, (err) => {
done(err);
});
});

it('correctly handles failed GET request', (cb) => {
sinon.stub(_request as any, 'req').callsFake(options => {
_options = options as CliRequestOptions;
Expand Down
20 changes: 19 additions & 1 deletion src/request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Axios, { AxiosError, AxiosInstance, AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios';
import Axios, { AxiosError, AxiosInstance, AxiosPromise, AxiosProxyConfig, AxiosRequestConfig, AxiosResponse } from 'axios';
import { Stream } from 'stream';
import auth, { Auth, CloudType } from './Auth.js';
import { Logger } from './cli/Logger.js';
Expand Down Expand Up @@ -183,6 +183,11 @@ class Request {
options.headers.authorization = `Bearer ${accessToken}`;
}
}

const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
if (proxyUrl) {
options.proxy = this.createProxyConfigFromUrl(proxyUrl);
}
return this.req(options);
})
.then((res: any): void => {
Expand Down Expand Up @@ -231,6 +236,19 @@ class Request {
const cloudUrl: string = Auth.getEndpointForResource(hostname, cloudType);
options.url = options.url!.replace(hostname, cloudUrl);
}

private createProxyConfigFromUrl(url: string): AxiosProxyConfig {
const parsedUrl = new URL(url);
const port = parsedUrl.port || (url.toLowerCase().startsWith('https') ? 443 : 80);
let authObject = null;
if (parsedUrl.username && parsedUrl.password) {
authObject = {
username: parsedUrl.username,
password: parsedUrl.password
};
}
return { host: parsedUrl.hostname, port: Number(port), protocol: 'http', ...(authObject && { auth: authObject }) };
}
}

export default new Request();
1 change: 1 addition & 0 deletions src/settingsNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const settingsNames = {
printErrorsAsPlainText: 'printErrorsAsPlainText',
prompt: 'prompt',
promptListPageSize: 'promptListPageSize',
proxyUrl: 'proxyUrl',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove it since we're not using it

showHelpOnFailure: 'showHelpOnFailure',
showSpinner: 'showSpinner'
};
Expand Down
2 changes: 1 addition & 1 deletion src/telemetryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ try {
}
appInsights.flush();
}
catch { }
catch { }