From af7d1472240989ba7cccbc8a85fd4f1c8c544234 Mon Sep 17 00:00:00 2001 From: yilmazbahadir Date: Tue, 17 Aug 2021 12:36:40 +0100 Subject: [PATCH] feat: Native HTTPS support (#641) * feat: Native SSL support * fix: PR#641 feedbacks, refactoring * fix: unit test for a successfull HTTPS call added * feedback fixes reverted style changes Co-authored-by: Baha Co-authored-by: fernando --- client/rest/.gitignore | 2 +- client/rest/rest/resources/rest.json | 3 + client/rest/rest/src/index.js | 2 +- client/rest/rest/src/server/bootstrapper.js | 68 ++++++++++++--- .../rest/test/server/bootstrapper_spec.js | 85 +++++++++++++++++-- .../rest/rest/test/server/certs/restSSL.crt | 33 +++++++ .../rest/rest/test/server/certs/restSSL.key | 52 ++++++++++++ 7 files changed, 224 insertions(+), 21 deletions(-) create mode 100644 client/rest/rest/test/server/certs/restSSL.crt create mode 100644 client/rest/rest/test/server/certs/restSSL.key diff --git a/client/rest/.gitignore b/client/rest/.gitignore index 52b2b37b..cfabe844 100644 --- a/client/rest/.gitignore +++ b/client/rest/.gitignore @@ -15,6 +15,6 @@ jsconfig.json rest-*.json rest/coverage -/rest/target/ +/rest/target* /rest/logs.log /catapult-rest.iml diff --git a/client/rest/rest/resources/rest.json b/client/rest/rest/resources/rest.json index 454f7af8..d27949c6 100644 --- a/client/rest/rest/resources/rest.json +++ b/client/rest/rest/resources/rest.json @@ -5,6 +5,9 @@ }, "port": 3000, + "protocol": "HTTPS", + "sslKeyPath": "", + "sslCertificatePath": "", "crossDomain": { "allowedHosts": ["*"], "allowedMethods": ["GET", "POST", "PUT", "OPTIONS"] diff --git a/client/rest/rest/src/index.js b/client/rest/rest/src/index.js index 8a680db2..63b426a5 100644 --- a/client/rest/rest/src/index.js +++ b/client/rest/rest/src/index.js @@ -113,7 +113,7 @@ const createServer = config => { ws: messageFormattingRules }); return { - server: bootstrapper.createServer(config.crossDomain, formatters.create(modelSystem.formatters), config.throttling), + server: bootstrapper.createServer(config, formatters.create(modelSystem.formatters), config.throttling), codec: modelSystem.codec }; }; diff --git a/client/rest/rest/src/server/bootstrapper.js b/client/rest/rest/src/server/bootstrapper.js index 2e9f7110..9edce4e5 100644 --- a/client/rest/rest/src/server/bootstrapper.js +++ b/client/rest/rest/src/server/bootstrapper.js @@ -27,6 +27,7 @@ const restify = require('restify'); const restifyErrors = require('restify-errors'); const winston = require('winston'); const WebSocket = require('ws'); +const fs = require('fs'); const isPromise = object => object && object.catch; @@ -88,37 +89,84 @@ const catapultRestifyPlugins = { } }; +const readSSLFileSync = (path, fileType, pathProperty) => { + if (!path) { + throw new Error( + `No SSL ${fileType} found, '${pathProperty}' property in the configuration must be provided.` + ); + } + try { + return fs.readFileSync(path); + } catch (err) { + if ('ENOENT' === err.code) { + throw new Error( + `SSL ${fileType} file cannot be found at the path: ${path}` + ); + } else { + throw err; + } + } +}; + module.exports = { createCrossDomainHeaderAdder, /** * Creates a REST api server. - * @param {array} crossDomainConfig Configuration related to access control, contains allowed host and HTTP methods. + * @param {object} config Application configuration (see rest.json). * @param {object} formatters Formatters to use for formatting responses. * @param {object} throttlingConfig Throttling configuration parameters, if not provided throttling won't be enabled. * @returns {object} Server. */ - createServer: (crossDomainConfig, formatters, throttlingConfig) => { - // create the server using a custom formatter - const server = restify.createServer({ + createServer: (config, formatters, throttlingConfig) => { + if (!config) + throw new Error('Config must be provided!'); + + if (!config.protocol) { + winston.warn( + 'Protocol(HTTPS|HTTP) is not configured explicitly in the configuration, defaulting to HTTPS.' + ); + } + + const protocol = config.protocol || 'HTTPS'; + winston.info(`Using protocol: ${protocol}`); + + const serverOptions = { name: '', // disable server header in response formatters: { 'application/json': formatters.json - } - }); + }, + ...('HTTPS' === protocol + ? { + key: readSSLFileSync(config.sslKeyPath, 'Key', 'sslKeyPath'), + certificate: readSSLFileSync( + config.sslCertificatePath, + 'Certificate', + 'sslCertificatePath' + ) + } + : {}) + }; + + // create the server using a custom formatter + const server = restify.createServer(serverOptions); // only allow application/json server.pre(catapultRestifyPlugins.body()); - const addCrossDomainHeaders = createCrossDomainHeaderAdder(crossDomainConfig || {}); + // config.crossDomain: Configuration related to access control, contains allowed host and HTTP methods. + if (!config.crossDomain) + winston.warn('CORS was not enabled - configuration incomplete'); + + const addCrossDomainHeaders = createCrossDomainHeaderAdder( + config.crossDomain || {} + ); + server.use(catapultRestifyPlugins.crossDomain(addCrossDomainHeaders)); server.use(restify.plugins.acceptParser('application/json')); server.use(restify.plugins.queryParser({ mapParams: true, parseArrays: false })); server.use(restify.plugins.jsonBodyParser({ mapParams: true })); - if (!crossDomainConfig) - winston.warn('CORS was not enabled - configuration incomplete'); - if (throttlingConfig) { if (throttlingConfig.burst && throttlingConfig.rate) { server.use(restify.plugins.throttle({ diff --git a/client/rest/rest/test/server/bootstrapper_spec.js b/client/rest/rest/test/server/bootstrapper_spec.js index dfa28c38..50b30e91 100644 --- a/client/rest/rest/test/server/bootstrapper_spec.js +++ b/client/rest/rest/test/server/bootstrapper_spec.js @@ -145,12 +145,12 @@ const createFormatters = options => formatters.create({ }); const createServer = options => { - const server = bootstrapper.createServer((options || {}).crossDomain, createFormatters(options)); + const server = bootstrapper.createServer((options || {}), createFormatters(options)); servers.push(server); return server; }; -const createWebSocketServer = () => createServer({ formatterName: 'ws' }); +const createWebSocketServer = () => createServer({ protocol: 'HTTP', formatterName: 'ws' }); describe('server (bootstrapper)', () => { afterEach(() => { @@ -173,7 +173,7 @@ describe('server (bootstrapper)', () => { const spy = sinon.spy(restify.plugins, 'throttle'); // Act: - bootstrapper.createServer({}, createFormatters(), throttlingConfig); + bootstrapper.createServer({ protocol: 'HTTP' }, createFormatters(), throttlingConfig); // Assert: expect(spy.calledOnceWith({ @@ -191,7 +191,7 @@ describe('server (bootstrapper)', () => { const spy = sinon.spy(restify.plugins, 'throttle'); // Act: - bootstrapper.createServer({}, createFormatters()); + bootstrapper.createServer({ protocol: 'HTTP' }, createFormatters()); // Assert: expect(spy.notCalled).to.equal(true); @@ -207,7 +207,7 @@ describe('server (bootstrapper)', () => { const logSpy = sinon.spy(winston, 'warn'); // Act: - bootstrapper.createServer({}, createFormatters(), { burst: 20 }); + bootstrapper.createServer({ protocol: 'HTTP' }, createFormatters(), { burst: 20 }); spy.restore(); logSpy.restore(); @@ -224,7 +224,7 @@ describe('server (bootstrapper)', () => { const logSpy = sinon.spy(winston, 'warn'); // Act: - bootstrapper.createServer({}, createFormatters(), { rate: 20 }); + bootstrapper.createServer({ protocol: 'HTTP' }, createFormatters(), { rate: 20 }); spy.restore(); logSpy.restore(); @@ -246,7 +246,7 @@ describe('server (bootstrapper)', () => { }; const makeJsonHippie = (route, method, options) => { - const server = createServer(options); + const server = createServer({ ...options, protocol: 'HTTP' }); addRestRoutes(server); const mockServer = hippie(server).json()[method](route); @@ -421,7 +421,7 @@ describe('server (bootstrapper)', () => { const spy = sinon.spy(winston, 'warn'); // Act: - bootstrapper.createServer(undefined, createFormatters()); + bootstrapper.createServer({ protocol: 'HTTP' }, createFormatters()); spy.restore(); // Assert: @@ -647,7 +647,10 @@ describe('server (bootstrapper)', () => { describe('OPTIONS', () => { const makeJsonHippieForOptions = route => { - const server = createServer({ crossDomain: { allowedMethods: ['FOO', 'OPTIONS', 'BAR'], allowedHosts: ['*'] } }); + const server = createServer({ + protocol: 'HTTP', + crossDomain: { allowedMethods: ['FOO', 'OPTIONS', 'BAR'], allowedHosts: ['*'] } + }); const routeHandler = (req, res, next) => { res.send(200); next(); @@ -751,6 +754,70 @@ describe('server (bootstrapper)', () => { }); }); + describe('HTTPS', () => { + it('creates https server with certificate and key given', done => { + createServer({ + port: 3001, + protocol: 'HTTPS', + sslKeyPath: `${__dirname}/certs/restSSL.key`, + sslCertificatePath: `${__dirname}/certs/restSSL.crt` + }); + done(); + }); + + it('throws error when the key path is missing', done => { + expect(() => createServer({ port: 3001, protocol: 'HTTPS', sslCertificatePath: `${__dirname}/certs/restSSL.crt` })) + .to.throw('No SSL Key found, \'sslKeyPath\' property in the configuration must be provided.'); + done(); + }); + + it('throws error when the certificate path is missing', done => { + expect(() => createServer({ port: 3001, protocol: 'HTTPS', sslKeyPath: `${__dirname}/certs/restSSL.key` })) + .to.throw('No SSL Certificate found, ' + + '\'sslCertificatePath\' property in the configuration must be provided.'); + done(); + }); + + it('starts https and throws error when the protocol is not defined', done => { + expect(() => createServer({ port: 3001 })).to.throw(); + done(); + }); + + it('starts http when the protocol is HTTP', done => { + createServer({ port: 3001, protocol: 'HTTP' }); + done(); + }); + + it('handles HTTPS routes successfully', done => { + // For unit testing, the unit test client ignores self-signed certificate errors. + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + + const httpsPort = 3001; + const server = createServer({ + port: httpsPort, + protocol: 'HTTPS', + sslKeyPath: `${__dirname}/certs/restSSL.key`, + sslCertificatePath: `${__dirname}/certs/restSSL.crt` + }); + + addRestRoutes(server); + server.listen(httpsPort); + + hippie() + .header('User-Agent', 'hippie') + .json() + .get(`https://127.0.0.1:${httpsPort}/dummy/${dummyIds.valid}`) + .expectStatus(200) + .end((err, res, body) => { + expect(body).to.deep.equal({ + id: 123, + current: { height: [10, 0], scoreLow: [16, 0], scoreHigh: [11, 0] } + }); + done(); + }); + }); + }); + describe('websockets', () => { // note: although rest server implementation uses single websocket route ('/ws'), // server.ws allows you to register any name and you can register multiple different routes diff --git a/client/rest/rest/test/server/certs/restSSL.crt b/client/rest/rest/test/server/certs/restSSL.crt new file mode 100644 index 00000000..4011881c --- /dev/null +++ b/client/rest/rest/test/server/certs/restSSL.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/client/rest/rest/test/server/certs/restSSL.key b/client/rest/rest/test/server/certs/restSSL.key new file mode 100644 index 00000000..3059e2a3 --- /dev/null +++ b/client/rest/rest/test/server/certs/restSSL.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +ZLeHTEemDGIzL0gEuV48U/ObVhoo +-----END PRIVATE KEY-----