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

Support for specifying an http.Agent #402

Merged
merged 14 commits into from
Dec 7, 2018
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Unreleased

- [added] `AppOptions` now accepts an optional `http.Agent` object. The
`http.Agent` specified via this API is used when the SDK makes backend
HTTP calls. This can be used when it is required to deploy the Admin SDK
behind a proxy.
- [added] `admin.credential.cert()`, `admin.credential.applicationDefault()`,
and `admin.credential.refreshToken()` methods now accept an `http.Agent`
as an optional argument. If specified, the `http.Agent` will be used
when calling Google backend servers to fetch OAuth2 access tokens.
- [added] `messaging.AndroidNotification`type now supports channel_id.

# v6.3.0
Expand Down
2,460 changes: 1,117 additions & 1,343 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 21 additions & 7 deletions src/auth/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import path = require('path');

import {AppErrorCodes, FirebaseAppError} from '../utils/error';
import {HttpClient, HttpRequestConfig} from '../utils/api-request';

import {Agent} from 'http';

const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';
Expand Down Expand Up @@ -216,13 +216,16 @@ function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Pro
* Implementation of Credential that uses a service account certificate.
*/
export class CertCredential implements Credential {

private readonly certificate: Certificate;
private readonly httpClient: HttpClient;
private readonly httpAgent: Agent;

constructor(serviceAccountPathOrObject: string | object) {
constructor(serviceAccountPathOrObject: string | object, httpAgent?: Agent) {
this.certificate = (typeof serviceAccountPathOrObject === 'string') ?
Certificate.fromPath(serviceAccountPathOrObject) : new Certificate(serviceAccountPathOrObject);
this.httpClient = new HttpClient();
this.httpAgent = httpAgent;
}

public getAccessToken(): Promise<GoogleOAuthAccessToken> {
Expand All @@ -236,6 +239,7 @@ export class CertCredential implements Credential {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: postData,
httpAgent: this.httpAgent,
};
return requestAccessToken(this.httpClient, request);
}
Expand Down Expand Up @@ -278,13 +282,16 @@ export interface Credential {
* Implementation of Credential that gets access tokens from refresh tokens.
*/
export class RefreshTokenCredential implements Credential {

private readonly refreshToken: RefreshToken;
private readonly httpClient: HttpClient;
private readonly httpAgent: Agent;

constructor(refreshTokenPathOrObject: string | object) {
constructor(refreshTokenPathOrObject: string | object, httpAgent?: Agent) {
this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ?
RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject);
this.httpClient = new HttpClient();
this.httpAgent = httpAgent;
}

public getAccessToken(): Promise<GoogleOAuthAccessToken> {
Expand All @@ -300,6 +307,7 @@ export class RefreshTokenCredential implements Credential {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: postData,
httpAgent: this.httpAgent,
};
return requestAccessToken(this.httpClient, request);
}
Expand All @@ -318,11 +326,17 @@ export class RefreshTokenCredential implements Credential {
export class MetadataServiceCredential implements Credential {

private readonly httpClient = new HttpClient();
private readonly httpAgent: Agent;

constructor(httpAgent?: Agent) {
this.httpAgent = httpAgent;
}

public getAccessToken(): Promise<GoogleOAuthAccessToken> {
const request: HttpRequestConfig = {
method: 'GET',
url: `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_PATH}`,
httpAgent: this.httpAgent,
};
return requestAccessToken(this.httpClient, request);
}
Expand All @@ -340,21 +354,21 @@ export class MetadataServiceCredential implements Credential {
export class ApplicationDefaultCredential implements Credential {
private credential_: Credential;

constructor() {
constructor(httpAgent?: Agent) {
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
const serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS);
this.credential_ = new CertCredential(serviceAccount);
this.credential_ = new CertCredential(serviceAccount, httpAgent);
return;
}

// It is OK to not have this file. If it is present, it must be valid.
const refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH);
if (refreshToken) {
this.credential_ = new RefreshTokenCredential(refreshToken);
this.credential_ = new RefreshTokenCredential(refreshToken, httpAgent);
return;
}

this.credential_ = new MetadataServiceCredential();
this.credential_ = new MetadataServiceCredential(httpAgent);
}

public getAccessToken(): Promise<GoogleOAuthAccessToken> {
Expand Down
3 changes: 3 additions & 0 deletions src/firebase-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {FirestoreService} from './firestore/firestore';
import {InstanceId} from './instance-id/instance-id';
import {ProjectManagement} from './project-management/project-management';

import {Agent} from 'http';

/**
* Type representing a callback which is called every time an app lifecycle event occurs.
*/
Expand All @@ -46,6 +48,7 @@ export interface FirebaseAppOptions {
serviceAccountId?: string;
storageBucket?: string;
projectId?: string;
httpAgent?: Agent;
}

/**
Expand Down
14 changes: 8 additions & 6 deletions src/firebase-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import fs = require('fs');
import {Agent} from 'http';
import {deepExtend} from './utils/deep-copy';
import {AppErrorCodes, FirebaseAppError} from './utils/error';
import {AppHook, FirebaseApp, FirebaseAppOptions} from './firebase-app';
Expand Down Expand Up @@ -270,25 +271,26 @@ export class FirebaseNamespaceInternals {


const firebaseCredential = {
cert: (serviceAccountPathOrObject: string | object): Credential => {
cert: (serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential => {
const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject);
if (!(stringifiedServiceAccount in globalCertCreds)) {
globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject);
globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject, httpAgent);
}
return globalCertCreds[stringifiedServiceAccount];
},

refreshToken: (refreshTokenPathOrObject: string | object): Credential => {
refreshToken: (refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential => {
const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject);
if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) {
globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential(refreshTokenPathOrObject);
globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential(
refreshTokenPathOrObject, httpAgent);
}
return globalRefreshTokenCreds[stringifiedRefreshToken];
},

applicationDefault: (): Credential => {
applicationDefault: (httpAgent?: Agent): Credential => {
if (typeof globalAppDefaultCred === 'undefined') {
globalAppDefaultCred = new ApplicationDefaultCredential();
globalAppDefaultCred = new ApplicationDefaultCredential(httpAgent);
}
return globalAppDefaultCred;
},
Expand Down
8 changes: 5 additions & 3 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {Bucket} from '@google-cloud/storage';
import * as _firestore from '@google-cloud/firestore';
import {Agent} from 'http';

declare namespace admin {
interface FirebaseError {
Expand Down Expand Up @@ -49,6 +50,7 @@ declare namespace admin {
serviceAccountId?: string;
storageBucket?: string;
projectId?: string;
httpAgent?: Agent;
}

var SDK_VERSION: string;
Expand Down Expand Up @@ -269,9 +271,9 @@ declare namespace admin.credential {
getAccessToken(): Promise<admin.GoogleOAuthAccessToken>;
}

function applicationDefault(): admin.credential.Credential;
function cert(serviceAccountPathOrObject: string|admin.ServiceAccount): admin.credential.Credential;
function refreshToken(refreshTokenPathOrObject: string|Object): admin.credential.Credential;
function applicationDefault(httpAgent?: Agent): admin.credential.Credential;
function cert(serviceAccountPathOrObject: string|admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential;
function refreshToken(refreshTokenPathOrObject: string|Object, httpAgent?: Agent): admin.credential.Credential;
}

declare namespace admin.database {
Expand Down
14 changes: 12 additions & 2 deletions src/utils/api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface HttpRequestConfig {
data?: string | object | Buffer;
/** Connect and read timeout (in milliseconds) for the outgoing request. */
timeout?: number;
httpAgent?: http.Agent;
}

/**
Expand Down Expand Up @@ -228,11 +229,16 @@ function sendRequest(httpRequestConfig: HttpRequestConfig): Promise<LowLevelResp
const parsed = url.parse(fullUrl);
const protocol = parsed.protocol || 'https:';
const isHttps = protocol === 'https:';
const options = {
let port: string = parsed.port;
if (!port) {
port = isHttps ? '443' : '80';
}
const options: https.RequestOptions = {
hostname: parsed.hostname,
port: parsed.port,
port,
path: parsed.path,
method: config.method,
agent: config.httpAgent,
headers,
};
const transport: any = isHttps ? https : http;
Expand Down Expand Up @@ -360,6 +366,10 @@ export class AuthorizedHttpClient extends HttpClient {
requestCopy.headers = requestCopy.headers || {};
const authHeader = 'Authorization';
requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`;

if (!requestCopy.httpAgent && this.app.options.httpAgent) {
requestCopy.httpAgent = this.app.options.httpAgent;
}
return super.send(requestCopy);
});
}
Expand Down
4 changes: 3 additions & 1 deletion test/resources/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export function mockCredentialApp(): FirebaseApp {
}

export function appWithOptions(options: FirebaseAppOptions): FirebaseApp {
return new FirebaseApp(options, appName, new FirebaseNamespace().INTERNAL);
const namespaceInternals = new FirebaseNamespace().INTERNAL;
namespaceInternals.removeApp = _.noop;
return new FirebaseApp(options, appName, namespaceInternals);
}

export function appReturningNullAccessToken(): FirebaseApp {
Expand Down
74 changes: 66 additions & 8 deletions test/unit/auth/credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
MetadataServiceCredential, RefreshTokenCredential,
} from '../../../src/auth/credential';
import { HttpClient } from '../../../src/utils/api-request';
import {Agent} from 'https';

chai.should();
chai.use(sinonChai);
Expand All @@ -45,6 +46,12 @@ const expect = chai.expect;
let TEST_GCLOUD_CREDENTIALS: any;
const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json';
const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME, '.config', GCLOUD_CREDENTIAL_SUFFIX);
const MOCK_REFRESH_TOKEN_CONFIG = {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
type: 'authorized_user',
refresh_token: 'test_token',
};
try {
TEST_GCLOUD_CREDENTIALS = JSON.parse(fs.readFileSync(GCLOUD_CREDENTIAL_PATH).toString());
} catch (error) {
Expand Down Expand Up @@ -83,7 +90,6 @@ const FIVE_MINUTES_IN_SECONDS = 5 * 60;


describe('Credential', () => {
let mockedRequests: nock.Scope[] = [];
let mockCertificateObject: any;
let oldProcessEnv: NodeJS.ProcessEnv;

Expand All @@ -97,8 +103,6 @@ describe('Credential', () => {
});

afterEach(() => {
_.forEach(mockedRequests, (mockedRequest) => mockedRequest.done());
mockedRequests = [];
process.env = oldProcessEnv;
});

Expand Down Expand Up @@ -280,11 +284,7 @@ describe('Credential', () => {

describe('RefreshTokenCredential', () => {
it('should not return a certificate', () => {
if (skipAndLogWarningIfNoGcloud()) {
return;
}

const c = new RefreshTokenCredential(TEST_GCLOUD_CREDENTIALS);
const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG);
expect(c.getCertificate()).to.be.null;
});

Expand Down Expand Up @@ -396,4 +396,62 @@ describe('Credential', () => {
});
});
});

describe('HTTP Agent', () => {
const expectedToken = utils.generateRandomAccessToken();
let stub: sinon.SinonStub;

beforeEach(() => {
stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({
access_token: expectedToken,
token_type: 'Bearer',
expires_in: 60 * 60,
}));
});

afterEach(() => {
stub.restore();
});

it('CertCredential should use the provided HTTP Agent', () => {
const agent = new Agent();
const c = new CertCredential(mockCertificateObject, agent);
return c.getAccessToken().then((token) => {
expect(token.access_token).to.equal(expectedToken);
expect(stub).to.have.been.calledOnce;
expect(stub.args[0][0].httpAgent).to.equal(agent);
});
});

it('RefreshTokenCredential should use the provided HTTP Agent', () => {
const agent = new Agent();
const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG, agent);
return c.getAccessToken().then((token) => {
expect(token.access_token).to.equal(expectedToken);
expect(stub).to.have.been.calledOnce;
expect(stub.args[0][0].httpAgent).to.equal(agent);
});
});

it('MetadataServiceCredential should use the provided HTTP Agent', () => {
const agent = new Agent();
const c = new MetadataServiceCredential(agent);
return c.getAccessToken().then((token) => {
expect(token.access_token).to.equal(expectedToken);
expect(stub).to.have.been.calledOnce;
expect(stub.args[0][0].httpAgent).to.equal(agent);
});
});

it('ApplicationDefaultCredential should use the provided HTTP Agent', () => {
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json');
const agent = new Agent();
const c = new ApplicationDefaultCredential(agent);
return c.getAccessToken().then((token) => {
expect(token.access_token).to.equal(expectedToken);
expect(stub).to.have.been.calledOnce;
expect(stub.args[0][0].httpAgent).to.equal(agent);
});
});
});
});
Loading