Skip to content

Commit b9cee01

Browse files
committed
feat(ssl): cafile, certfile, keyfile options
addresses ionic-team#1774 (comment)
1 parent 706680d commit b9cee01

23 files changed

Lines changed: 209 additions & 105 deletions

File tree

packages/@ionic/cli-utils/src/definitions.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export interface SecurityProfile {
186186

187187
export interface IApp {
188188
load(app_id?: string): Promise<AppDetails>;
189-
list(): IPaginator<Response<AppDetails[]>>;
189+
paginate(): Promise<IPaginator<Response<AppDetails[]>>>;
190190
create(app: { name: string; }): Promise<AppDetails>;
191191
}
192192

@@ -344,6 +344,11 @@ export interface ConfigFile {
344344
api: string;
345345
dash: string;
346346
};
347+
ssl?: {
348+
cafile?: string | string[];
349+
certfile?: string | string[];
350+
keyfile?: string | string[];
351+
};
347352
git: {
348353
host: string;
349354
port?: number;
@@ -408,11 +413,11 @@ export interface APIResponseSuccess {
408413
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'PURGE' | 'HEAD' | 'OPTIONS';
409414

410415
export interface IClient {
411-
host: string;
416+
config: IConfig;
412417

413-
make(method: HttpMethod, path: string): superagentType.SuperAgentRequest;
418+
make(method: HttpMethod, path: string): Promise<{ req: superagentType.SuperAgentRequest; }>;
414419
do(req: superagentType.SuperAgentRequest): Promise<APIResponseSuccess>;
415-
paginate<T extends Response<Object[]>>(reqgen: () => superagentType.SuperAgentRequest, guard: (res: APIResponseSuccess) => res is T): IPaginator<T>;
420+
paginate<T extends Response<Object[]>>(reqgen: () => Promise<{ req: superagentType.SuperAgentRequest; }>, guard: (res: APIResponseSuccess) => res is T): Promise<IPaginator<T>>;
416421
}
417422

418423
export interface IPaginator<T extends Response<Object[]>> extends IterableIterator<Promise<T>> {}

packages/@ionic/cli-utils/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export function registerHooks(hooks: IHookEngine) {
6767
const wasLoggedIn = await env.session.isLoggedIn();
6868
await env.session.logout();
6969

70-
env.client.host = config.urls.api;
7170
env.session = await getSession(env.config, env.project, env.client);
7271

7372
if (wasLoggedIn) {
@@ -132,7 +131,7 @@ export async function generateIonicEnvironment(plugin: RootPlugin, pargv: string
132131
env['IONIC_PROJECT_FILE'] = PROJECT_FILE;
133132

134133
const project = new Project(env['IONIC_PROJECT_DIR'], PROJECT_FILE);
135-
const client = new Client(configData.urls.api);
134+
const client = new Client(config);
136135
const session = await getSession(config, project, client);
137136
const hooks = new HookEngine();
138137
const telemetry = new Telemetry({ config, client, plugin, project, session });

packages/@ionic/cli-utils/src/lib/app.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export class App implements IApp {
1313
constructor(public token: string, protected client: IClient) {}
1414

1515
async load(app_id: string): Promise<AppDetails> {
16-
const req = this.client.make('GET', `/apps/${app_id}`)
16+
let { req } = await this.client.make('GET', `/apps/${app_id}`);
17+
req = req
1718
.set('Authorization', `Bearer ${this.token}`)
1819
.send({});
1920
const res = await this.client.do(req);
@@ -25,15 +26,20 @@ export class App implements IApp {
2526
return res.data;
2627
}
2728

28-
list(): IPaginator<Response<AppDetails[]>> {
29+
async paginate(): Promise<IPaginator<Response<AppDetails[]>>> {
2930
return this.client.paginate(
30-
() => this.client.make('GET', '/apps').set('Authorization', `Bearer ${this.token}`),
31+
async () => {
32+
let { req } = await this.client.make('GET', '/apps');
33+
req = req.set('Authorization', `Bearer ${this.token}`);
34+
return { req };
35+
},
3136
isAppsResponse,
3237
);
3338
}
3439

3540
async create({ name }: { name: string; }) {
36-
const req = this.client.make('POST', '/apps')
41+
let { req } = await this.client.make('POST', '/apps');
42+
req = req
3743
.set('Authorization', `Bearer ${this.token}`)
3844
.send({ name });
3945
const res = await this.client.do(req);

packages/@ionic/cli-utils/src/lib/cordova/__tests__/resources.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path';
22
import * as fsSpy from '../../utils/fs';
3-
import * as httpSpy from '../../utils/http';
3+
import * as httpSpy from '../../http';
44
import * as resources from '../resources';
55

66
import { ImageResource, SourceImage } from '../../../definitions';
@@ -276,7 +276,7 @@ describe('@ionic/cli-utils', () => {
276276
describe('uploadSourceImages', () => {
277277
it('should upload an image and receive back metadata', async () => {
278278
const createRequestSpy = jest.spyOn(httpSpy, 'createRequest');
279-
const createRequestMock = {
279+
const createRequestMock = Promise.resolve({ req: {
280280
type: jest.fn().mockReturnThis(),
281281
attach: jest.fn().mockReturnThis(),
282282
field: jest.fn(() => Promise.resolve({
@@ -288,7 +288,7 @@ describe('@ionic/cli-utils', () => {
288288
Vector: false
289289
}
290290
}))
291-
};
291+
}});
292292

293293
createRequestSpy.mockImplementationOnce(() => createRequestMock);
294294

@@ -303,7 +303,7 @@ describe('@ionic/cli-utils', () => {
303303
imageId: '60278b0fa1d5abf43d07c5ae0f8a0b41'
304304
}];
305305

306-
const response = await resources.uploadSourceImages(sourceImages, false);
306+
const response = await resources.uploadSourceImages({ config: undefined }, sourceImages);
307307
expect(response).toEqual([{
308308
Error: '',
309309
Width: 337,
@@ -318,12 +318,16 @@ describe('@ionic/cli-utils', () => {
318318
it('should upload an image and write a stream to the destination', async () => {
319319
jest.spyOn(fsSpy, 'writeStreamToFile').mockImplementationOnce(() => Promise.resolve());
320320
const createRequestSpy = jest.spyOn(httpSpy, 'createRequest');
321-
const createRequestMock = {
322-
type: jest.fn().mockReturnThis(),
323-
send: jest.fn().mockReturnThis(),
324-
on: jest.fn().mockReturnThis(),
321+
const requestMock = {
322+
req: {
323+
type: jest.fn().mockReturnThis(),
324+
send: jest.fn().mockReturnThis(),
325+
on: jest.fn().mockReturnThis(),
326+
}
325327
};
326328

329+
const createRequestMock = Promise.resolve(requestMock);
330+
327331
createRequestSpy.mockImplementationOnce(() => createRequestMock);
328332

329333
const imgResource: ImageResource = {
@@ -339,10 +343,10 @@ describe('@ionic/cli-utils', () => {
339343
dest: path.join(__dirname, 'fixtures', 'drawable-land-ldpi-screen.png')
340344
};
341345

342-
await resources.transformResourceImage(imgResource, false);
346+
await resources.transformResourceImage({ config: undefined }, imgResource);
343347

344348
expect(fsSpy.writeStreamToFile).toHaveBeenCalledWith(
345-
createRequestMock, imgResource.dest
349+
requestMock.req, imgResource.dest
346350
);
347351
});
348352
});

packages/@ionic/cli-utils/src/lib/cordova/resources.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414

1515
import { flattenArray } from '../utils/array';
1616
import { copyDirectory, fsMkdirp, fsReadFile, fsStat, fsWriteFile, getFileChecksum, pathAccessible, pathExists, readDir, writeStreamToFile } from '../utils/fs';
17-
import { createRequest } from '../utils/http';
17+
import { createRequest } from '../http';
1818
import { ConfigXml } from './config';
1919

2020
const SUPPORTED_SOURCE_EXTENSIONS = ['.psd', '.ai', '.png'];
@@ -152,13 +152,15 @@ export function findMostSpecificImage(imageResource: ImageResource, srcImagesAva
152152
* Upload the provided source image through the resources web service. This will make it available
153153
* for transforms for the next 5 minutes.
154154
*/
155-
export async function uploadSourceImages(srcImages: SourceImage[]): Promise<ImageUploadResponse[]> {
155+
export async function uploadSourceImages(env: IonicEnvironment, srcImages: SourceImage[]): Promise<ImageUploadResponse[]> {
156156
return Promise.all(
157157
srcImages.map(async (srcImage) => {
158-
const res = await createRequest('POST', UPLOAD_URL)
158+
let { req } = await createRequest(env.config, 'POST', UPLOAD_URL);
159+
req = req
159160
.type('form')
160161
.attach('src', srcImage.path)
161162
.field('image_id', srcImage.imageId || '');
163+
const res = await req;
162164
return res.body;
163165
})
164166
);
@@ -168,9 +170,11 @@ export async function uploadSourceImages(srcImages: SourceImage[]): Promise<Imag
168170
* Using the transformation web service transform the provided image resource
169171
* into the appropriate w x h and then write this file to the provided destination directory.
170172
*/
171-
export async function transformResourceImage(imageResource: ImageResource) {
173+
export async function transformResourceImage(env: IonicEnvironment, imageResource: ImageResource) {
174+
let { req } = await createRequest(env.config, 'POST', TRANSFORM_URL);
175+
172176
return new Promise<void>((resolve, reject) => {
173-
const req = createRequest('POST', TRANSFORM_URL)
177+
req = req
174178
.type('form')
175179
.send({
176180
'name': imageResource.name,
@@ -260,7 +264,7 @@ async function ensureDefaultResources(env: IonicEnvironment): Promise<string> {
260264
if (recreateTmpDir) {
261265
const task = env.tasks.next(`Downloading default resources`);
262266

263-
await tarXvfFromUrl(DEFAULT_RESOURCES_URL, tmpResourcesDir, { progress: (loaded, total) => {
267+
await tarXvfFromUrl(env, DEFAULT_RESOURCES_URL, tmpResourcesDir, { progress: (loaded, total) => {
264268
task.progress(loaded, total);
265269
}});
266270

packages/@ionic/cli-utils/src/lib/deploy.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export class DeployClient {
77
constructor(protected appUserToken: string, protected client: IClient) {}
88

99
async getChannel(uuidOrTag: string): Promise<DeployChannel> {
10-
const req = this.client.make('GET', `/deploy/channels/${uuidOrTag}`)
10+
let { req } = await this.client.make('GET', `/deploy/channels/${uuidOrTag}`);
11+
req = req
1112
.set('Authorization', `Bearer ${this.appUserToken}`)
1213
.send();
1314

@@ -21,7 +22,8 @@ export class DeployClient {
2122
}
2223

2324
async deploy(snapshot: string, channel: string): Promise<Deploy> {
24-
const req = this.client.make('POST', '/deploy/deploys')
25+
let { req } = await this.client.make('POST', '/deploy/deploys');
26+
req = req
2527
.set('Authorization', `Bearer ${this.appUserToken}`)
2628
.send({ snapshot, channel });
2729

@@ -39,7 +41,8 @@ export class DeployClient {
3941
fields.push('url');
4042
}
4143

42-
const req = this.client.make('GET', `/deploy/snapshots/${uuid}`)
44+
let { req } = await this.client.make('GET', `/deploy/snapshots/${uuid}`);
45+
req = req
4346
.set('Authorization', `Bearer ${this.appUserToken}`)
4447
.query({ fields })
4548
.send();
@@ -56,7 +59,8 @@ export class DeployClient {
5659
async requestSnapshotUpload(options: { legacy_duplication?: string; note?: string; user_metadata?: Object } = {}): Promise<DeploySnapshotRequest> {
5760
options.legacy_duplication = '1';
5861

59-
const req = this.client.make('POST', '/deploy/snapshots')
62+
let { req } = await this.client.make('POST', '/deploy/snapshots');
63+
req = req
6064
.set('Authorization', `Bearer ${this.appUserToken}`)
6165
.send(options);
6266

@@ -68,19 +72,20 @@ export class DeployClient {
6872

6973
// TODO: Remove updateMetaDataReq when POST -> deploy/snapshots accepts user_metadata
7074
if (options.user_metadata) {
71-
const updateMetaDataReq = this.client.make('PATCH', `/deploy/snapshots/${res.data.uuid}`)
75+
let { req } = await this.client.make('PATCH', `/deploy/snapshots/${res.data.uuid}`);
76+
req = req
7277
.set('Authorization', `Bearer ${this.appUserToken}`)
7378
.send({
7479
'user_metadata': options.user_metadata
7580
});
7681

77-
await this.client.do(updateMetaDataReq);
82+
await this.client.do(req);
7883
}
7984

8085
return res.data;
8186
}
8287

8388
uploadSnapshot(snapshot: DeploySnapshotRequest, zip: NodeJS.ReadableStream, progress?: (loaded: number, total: number) => void): Promise<void> {
84-
return s3SignedUpload(snapshot.presigned_post, zip, { progress });
89+
return s3SignedUpload(this.client.config, snapshot.presigned_post, zip, { progress });
8590
}
8691
}

packages/@ionic/cli-utils/src/lib/http.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
APIResponseSuccess,
1010
HttpMethod,
1111
IClient,
12+
IConfig,
1213
IPaginator,
1314
Response,
1415
SuperAgentError,
1516
} from '../definitions';
1617

1718
import { isAPIResponseError, isAPIResponseSuccess } from '../guards';
18-
import { createRequest } from './utils/http';
19+
import { getGlobalProxy } from './utils/http';
20+
import { fsReadFile } from './utils/fs';
1921
import { FatalException } from './errors';
2022

2123
const FORMAT_ERROR_BODY_MAX_LENGTH = 1000;
@@ -24,13 +26,74 @@ const CONTENT_TYPE_JSON = 'application/json';
2426
export const ERROR_UNKNOWN_CONTENT_TYPE = 'UNKNOWN_CONTENT_TYPE';
2527
export const ERROR_UNKNOWN_RESPONSE_FORMAT = 'UNKNOWN_RESPONSE_FORMAT';
2628

29+
let CAS: string[] | undefined;
30+
let CERTS: string[] | undefined;
31+
let KEYS: string[] | undefined;
32+
33+
export async function createRequest(config: IConfig, method: string, url: string): Promise<{ req: superagentType.SuperAgentRequest; }> {
34+
const superagent = await import('superagent');
35+
const c = await config.load();
36+
const [ proxy, ] = getGlobalProxy();
37+
38+
let req = superagent(method, url);
39+
40+
if (proxy && req.proxy) {
41+
req = req.proxy(proxy);
42+
}
43+
44+
if (c.ssl) {
45+
const conform = (p?: string | string[]): string[] => {
46+
if (!p) {
47+
return [];
48+
}
49+
50+
if (typeof p === 'string') {
51+
return [p];
52+
}
53+
54+
return p;
55+
};
56+
57+
if (!CAS) {
58+
CAS = await Promise.all(conform(c.ssl.cafile).map(p => fsReadFile(p, { encoding: 'utf8' })));
59+
}
60+
61+
if (!CERTS) {
62+
CERTS = await Promise.all(conform(c.ssl.certfile).map(p => fsReadFile(p, { encoding: 'utf8' })));
63+
}
64+
65+
if (!KEYS) {
66+
KEYS = await Promise.all(conform(c.ssl.keyfile).map(p => fsReadFile(p, { encoding: 'utf8' })));
67+
}
68+
69+
if (CAS.length > 0) {
70+
req = req.ca(CAS);
71+
}
72+
73+
if (CERTS.length > 0) {
74+
req = req.cert(CERTS);
75+
}
76+
77+
if (KEYS.length > 0) {
78+
req = req.key(KEYS);
79+
}
80+
}
81+
82+
return { req };
83+
}
84+
2785
export class Client implements IClient {
28-
constructor(public host: string) {}
86+
constructor(public config: IConfig) {}
2987

30-
make(method: HttpMethod, path: string): superagentType.SuperAgentRequest {
31-
return createRequest(method, `${this.host}${path}`)
88+
async make(method: HttpMethod, path: string): Promise<{ req: superagentType.SuperAgentRequest; }> {
89+
const config = await this.config.load();
90+
const url = path.startsWith('http://') || path.startsWith('https://') ? path : `${config.urls.api}${path}`;
91+
let { req } = await createRequest(this.config, method, url);
92+
req = req
3293
.set('Content-Type', CONTENT_TYPE_JSON)
3394
.set('Accept', CONTENT_TYPE_JSON);
95+
96+
return { req };
3497
}
3598

3699
async do(req: superagentType.SuperAgentRequest): Promise<APIResponseSuccess> {
@@ -45,7 +108,7 @@ export class Client implements IClient {
45108
return r;
46109
}
47110

48-
paginate<T extends Response<Object[]>>(reqgen: () => superagentType.SuperAgentRequest, guard: (res: APIResponseSuccess) => res is T): Paginator<T> {
111+
async paginate<T extends Response<Object[]>>(reqgen: () => Promise<{ req: superagentType.SuperAgentRequest; }>, guard: (res: APIResponseSuccess) => res is T): Promise<Paginator<T>> {
49112
return new Paginator<T>(this, reqgen, guard);
50113
}
51114
}
@@ -56,7 +119,7 @@ export class Paginator<T extends Response<Object[]>> implements IPaginator<T> {
56119

57120
constructor(
58121
protected client: IClient,
59-
protected reqgen: () => superagentType.SuperAgentRequest,
122+
protected reqgen: () => Promise<{ req: superagentType.SuperAgentRequest; }>,
60123
protected guard: (res: APIResponseSuccess) => res is T,
61124
) {}
62125

@@ -68,7 +131,7 @@ export class Paginator<T extends Response<Object[]>> implements IPaginator<T> {
68131
return {
69132
done: false,
70133
value: (async () => {
71-
const req = this.reqgen();
134+
const { req } = await this.reqgen();
72135

73136
if (!this.previousReq) {
74137
this.previousReq = req;

0 commit comments

Comments
 (0)