diff --git a/.eslint-dictionary.json b/.eslint-dictionary.json index dee59decbab..f6324edd864 100644 --- a/.eslint-dictionary.json +++ b/.eslint-dictionary.json @@ -219,6 +219,7 @@ "junit", "jwks", "keyless", + "keyout", "keyphrase", "kinesis", "lang", @@ -278,6 +279,7 @@ "openid", "openpgp", "opensearch", + "openssl", "orgs", "Parti", "parens", diff --git a/packages/amplify-appsync-simulator/API.md b/packages/amplify-appsync-simulator/API.md index 66e60f6519a..5cd560e81e9 100644 --- a/packages/amplify-appsync-simulator/API.md +++ b/packages/amplify-appsync-simulator/API.md @@ -89,6 +89,8 @@ export class AmplifyAppSyncSimulator { // (undocumented) init(config: AmplifyAppSyncSimulatorConfig): void; // (undocumented) + get isHttps(): boolean; + // (undocumented) get localhostUrl(): string; // (undocumented) get pubsub(): PubSub; @@ -268,6 +270,10 @@ export type AppSyncSimulatorSchemaConfig = AppSyncMockFile; export type AppSyncSimulatorServerConfig = { port?: number; wsPort?: number; + httpsConfig?: { + sslKeyPath: string; + sslCertPath: string; + }; }; // @public (undocumented) diff --git a/packages/amplify-appsync-simulator/src/index.ts b/packages/amplify-appsync-simulator/src/index.ts index c0d3d1df4d2..4f5ad7aaa1e 100644 --- a/packages/amplify-appsync-simulator/src/index.ts +++ b/packages/amplify-appsync-simulator/src/index.ts @@ -184,6 +184,9 @@ export class AmplifyAppSyncSimulator { get localhostUrl(): string { return this._server.localhostUrl.graphql; } + get isHttps(): boolean { + return this._server.isHttps; + } get config(): AmplifyAppSyncSimulatorConfig { return this._config; } diff --git a/packages/amplify-appsync-simulator/src/server/index.ts b/packages/amplify-appsync-simulator/src/server/index.ts index aca397a1d8e..d533d61b7ee 100644 --- a/packages/amplify-appsync-simulator/src/server/index.ts +++ b/packages/amplify-appsync-simulator/src/server/index.ts @@ -2,6 +2,8 @@ import { OperationServer } from './operations'; import { AmplifyAppSyncSimulator } from '..'; import { AppSyncSimulatorServerConfig } from '../type-definition'; import { Server, createServer } from 'http'; +import { createServer as createHttpsServer } from 'https'; +import { readFileSync } from 'fs'; import { fromEvent } from 'promise-toolbox'; import { address as getLocalIpAddress } from 'ip'; import { AppSyncSimulatorSubscriptionServer } from './websocket-subscription'; @@ -17,10 +19,30 @@ export class AppSyncSimulatorServer { private _realTimeSubscriptionServer: AppSyncSimulatorSubscriptionServer; private _url: string; private _localhostUrl: string; + private _isHttps = false; constructor(private config: AppSyncSimulatorServerConfig, private simulatorContext: AmplifyAppSyncSimulator) { this._operationServer = new OperationServer(config, simulatorContext); - this._httpServer = createServer(this._operationServer.app); + + // Check if the https configuration is not provided + if (!config.httpsConfig) { + this._httpServer = createServer(this._operationServer.app); + } else { + try { + // Read the ssl cert and key + const sslOptions = { + key: readFileSync(config.httpsConfig.sslKeyPath), + cert: readFileSync(config.httpsConfig.sslCertPath), + }; + // Set the isHttps flag to true + this._isHttps = true; + // Create the https server + this._httpServer = createHttpsServer(sslOptions, this._operationServer.app); + } catch (e) { + throw new Error(`SSL key and certificate path provided are invalid. ${e.message}`); + } + } + this._realTimeSubscriptionServer = new AppSyncSimulatorSubscriptionServer( simulatorContext, this._httpServer, @@ -49,8 +71,9 @@ export class AppSyncSimulatorServer { this._httpServer.listen(port); await fromEvent(this._httpServer, 'listening').then(() => { - this._url = `http://${getLocalIpAddress()}:${port}`; - this._localhostUrl = `http://localhost:${port}`; + const protocol = this._isHttps ? 'https' : 'http'; + this._url = `${protocol}://${getLocalIpAddress()}:${port}`; + this._localhostUrl = `${protocol}://localhost:${port}`; }); } @@ -68,4 +91,7 @@ export class AppSyncSimulatorServer { graphql: this._localhostUrl, }; } + get isHttps() { + return this._isHttps; + } } diff --git a/packages/amplify-appsync-simulator/src/type-definition.ts b/packages/amplify-appsync-simulator/src/type-definition.ts index a98b5f87038..063b633aa47 100644 --- a/packages/amplify-appsync-simulator/src/type-definition.ts +++ b/packages/amplify-appsync-simulator/src/type-definition.ts @@ -152,6 +152,10 @@ export type AmplifyAppSyncSimulatorConfig = { export type AppSyncSimulatorServerConfig = { port?: number; wsPort?: number; + httpsConfig?: { + sslKeyPath: string; + sslCertPath: string; + }; }; export type AmplifyAppSyncSimulatorRequestContext = { diff --git a/packages/amplify-graphiql-explorer/README.md b/packages/amplify-graphiql-explorer/README.md index 2698c8a68dd..8a8021b987e 100644 --- a/packages/amplify-graphiql-explorer/README.md +++ b/packages/amplify-graphiql-explorer/README.md @@ -4,4 +4,4 @@ This is the package that contains graphiql explorer assets for amplify-appsync-s ## Development Mode -When making changes to grapiql-explore, run `yarn start`. All the requests get proxied to `http://localhost:20002/` +When making changes to grapiql-explorer, run `yarn start`. All the requests get proxied to `http://localhost:20002/` by default (If you use the --https flag on `amplify mock`, change the proxy from `http://localhost:20002/` to `https://localhost:20002/` in package.json.) diff --git a/packages/amplify-util-mock/src/__tests__/api/api.test.ts b/packages/amplify-util-mock/src/__tests__/api/api.test.ts index a9b031282c1..2af4ae8f935 100644 --- a/packages/amplify-util-mock/src/__tests__/api/api.test.ts +++ b/packages/amplify-util-mock/src/__tests__/api/api.test.ts @@ -135,6 +135,35 @@ describe('Test Mock API methods', () => { await expect(mockContext.print.error).toHaveBeenCalledWith('Failed to start API Mocking.'); }); + it('shows error message and resolution when https enabled if SSL key and certificate paths are not provided', async () => { + ConfigOverrideManager.getInstance = jest.fn().mockReturnValue(jest.fn); + const mockContext = { + print: { + red: jest.fn(), + green: jest.fn(), + error: jest.fn(), + }, + parameters: { + options: { + help: false, + }, + }, + input: { + argv: ['--https'], + }, + amplify: { + getEnvInfo: jest.fn().mockReturnValue({ projectPath: mockProjectRoot }), + pathManager: { + getGitIgnoreFilePath: jest.fn(), + }, + }, + } as unknown as $TSContext; + await run(mockContext); + await expect(mockContext.print.error).toHaveBeenCalledWith( + '\nThe --https option must be followed by the path to the SSL key and the path to the SSL certificate.\n', + ); + }); + it('attempts to set custom port correctly', async () => { const GRAPHQL_PORT = 8081; const mockContext = { diff --git a/packages/amplify-util-mock/src/__tests__/get-https-config.test.ts b/packages/amplify-util-mock/src/__tests__/get-https-config.test.ts new file mode 100644 index 00000000000..09c6f174ba4 --- /dev/null +++ b/packages/amplify-util-mock/src/__tests__/get-https-config.test.ts @@ -0,0 +1,36 @@ +import { getHttpsConfig } from '../utils/get-https-config'; + +describe('getHttpsConfig', () => { + let context; + + beforeEach(() => { + context = { + input: { + argv: [], + }, + print: { + error: jest.fn(), + }, + }; + }); + + it('returns paths when --https option is followed by key and cert paths', () => { + context.input.argv = ['--https', '/path/to/key', '/path/to/cert']; + + const config = getHttpsConfig(context); + + expect(config).toEqual({ + sslKeyPath: '/path/to/key', + sslCertPath: '/path/to/cert', + }); + }); + + it('returns null and prints error when --https option is not followed by key and cert paths', () => { + context.input.argv = ['--https']; + + const config = getHttpsConfig(context); + + expect(config).toEqual(null); + expect(context.print.error).toHaveBeenCalled(); + }); +}); diff --git a/packages/amplify-util-mock/src/api/api.ts b/packages/amplify-util-mock/src/api/api.ts index 8b197c0b406..861e5627d47 100644 --- a/packages/amplify-util-mock/src/api/api.ts +++ b/packages/amplify-util-mock/src/api/api.ts @@ -55,7 +55,12 @@ export class APITest { private userOverriddenSlots: string[] = []; private searchableTables: string[] = []; - async start(context, port: number = MOCK_API_PORT, wsPort: number = MOCK_API_PORT) { + async start( + context, + port: number = MOCK_API_PORT, + wsPort: number = MOCK_API_PORT, + httpsConfig?: { sslKeyPath: string; sslCertPath: string }, + ) { try { context.amplify.addCleanUpTask(async (context) => { await this.stop(context); @@ -72,6 +77,7 @@ export class APITest { this.appSyncSimulator = new AmplifyAppSyncSimulator({ port, wsPort, + httpsConfig: httpsConfig, }); await this.appSyncSimulator.start(); await this.resolverOverrideManager.start(); diff --git a/packages/amplify-util-mock/src/api/index.ts b/packages/amplify-util-mock/src/api/index.ts index 43ad6ce8e86..66074e3cd2a 100644 --- a/packages/amplify-util-mock/src/api/index.ts +++ b/packages/amplify-util-mock/src/api/index.ts @@ -1,6 +1,7 @@ import { APITest } from './api'; import { addMockDataToGitIgnore, addMockAPIResourcesToGitIgnore } from '../utils'; import { getMockConfig } from '../utils/mock-config-file'; +import { getHttpsConfig } from '../utils/get-https-config'; export async function start(context) { const testApi = new APITest(); @@ -8,7 +9,9 @@ export async function start(context) { addMockDataToGitIgnore(context); addMockAPIResourcesToGitIgnore(context); const mockConfig = await getMockConfig(context); - await testApi.start(context, mockConfig.graphqlPort, mockConfig.graphqlPort); + const httpsConfig = getHttpsConfig(context); + + await testApi.start(context, mockConfig.graphqlPort, mockConfig.graphqlPort, httpsConfig); } catch (e) { console.log(e); // Sending term signal so we clean up after ourselves diff --git a/packages/amplify-util-mock/src/utils/get-https-config.ts b/packages/amplify-util-mock/src/utils/get-https-config.ts new file mode 100644 index 00000000000..539d8350abb --- /dev/null +++ b/packages/amplify-util-mock/src/utils/get-https-config.ts @@ -0,0 +1,29 @@ +export function getHttpsConfig(context): { sslKeyPath: string; sslCertPath: string } | null { + if (!context.input || !context.input.argv) { + return null; + } + + const argv = context.input.argv; + const httpsIndex = argv.indexOf('--https'); + + if (httpsIndex !== -1) { + if (httpsIndex < argv.length - 2) { + const keyPath = argv[httpsIndex + 1]; + const certPath = argv[httpsIndex + 2]; + if (typeof keyPath === 'string' && typeof certPath === 'string') { + return { sslKeyPath: keyPath, sslCertPath: certPath }; + } else { + context.print.error('\nThe provided paths for the SSL key and certificate are not valid.\n'); + context.print.error('Please ensure you have entered the correct paths.\n'); + } + } else { + context.print.error('\nThe --https option must be followed by the path to the SSL key and the path to the SSL certificate.\n'); + context.print.error('Example: amplify mock api --https /path/to/key /path/to/cert\n'); + context.print.error('In order to generate a key and certificate, you can use openssl:\n'); + context.print.error('openssl req -nodes -new -x509 -keyout server.key -out server.cert\n'); + context.print.error('Then, run the command again with the paths to the generated key and certificate.\n'); + } + } + + return null; +}