Skip to content

Commit

Permalink
feat: Native HTTPS support (#641)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: fernando <[email protected]>
  • Loading branch information
3 people authored Aug 17, 2021
1 parent d573cdd commit af7d147
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 21 deletions.
2 changes: 1 addition & 1 deletion client/rest/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jsconfig.json
rest-*.json

rest/coverage
/rest/target/
/rest/target*
/rest/logs.log
/catapult-rest.iml
3 changes: 3 additions & 0 deletions client/rest/rest/resources/rest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
},

"port": 3000,
"protocol": "HTTPS",
"sslKeyPath": "",
"sslCertificatePath": "",
"crossDomain": {
"allowedHosts": ["*"],
"allowedMethods": ["GET", "POST", "PUT", "OPTIONS"]
Expand Down
2 changes: 1 addition & 1 deletion client/rest/rest/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
};
Expand Down
68 changes: 58 additions & 10 deletions client/rest/rest/src/server/bootstrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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({
Expand Down
85 changes: 76 additions & 9 deletions client/rest/rest/test/server/bootstrapper_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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({
Expand All @@ -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);
Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions client/rest/rest/test/server/certs/restSSL.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFwTCCA6mgAwIBAgIUAIYz+h52BpXbB8JEOZVqRM86Ck0wDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y
dGxhbmQxFTATBgNVBAoMDENvbXBhbnkgTmFtZTEMMAoGA1UECwwDT3JnMRgwFgYD
VQQDDA93d3cuZXhhbXBsZS5jb20wHhcNMjEwNzI4MTgyMjQ2WhcNMjIwNzI4MTgy
MjQ2WjBwMQswCQYDVQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQ
b3J0bGFuZDEVMBMGA1UECgwMQ29tcGFueSBOYW1lMQwwCgYDVQQLDANPcmcxGDAW
BgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAMIC1Xz11K6pVbw9rJdiYtn93MdeEkLdpWLj3Mw+lMOUrWuESgKtPuGj
/hB5C18sWWcDF4hofz8GU7AMyHo5jHiz54KmbcuZewp+SfL4UHUNOfwbFXpiO8nf
+hsVd9YY6GEF85iYg/sczfVDIjODCfONgI0t5QCiKvh/rIbe+zbn7pd40S9jFqeA
VngAXbzYZ+ExiOksdSkp2gUNdoDEHRXi007lUXK5prl1kff4TH/TlFLDiPJsN8ck
Zkgt/vtYdIcw4kxm0ToPBP4Mlxq8jBXqnq9bvtv+ypcUVi5BbsGJkw2cDOz0l1j/
aUXKRsXuKNGodM57h+Qt8pYSq1Ydxl4VtzhueyDscsOZNGK9rhinXKYQkV+yFzpY
/1j3OnOZlcOa/rJyYpR96SBxZFDXor1rQ79EwBLQlBQztFK2cgBisVAR/M4UqE5a
WUJwyWsX3Dq0gJ4oYT+10X8eu5+TNAVBUWJ7xmAo7iftjeoWrMow47ChE9mIhAqy
H2H+B4U2o2J9GJpOjOPe+yJaoBkAp71EYCReT5xv4J2y0Oa5LJyQ5f9EnvIoUGNe
fC8ZrTiPqvk0ztAr6ejdzxIhlny6+BON/nab0mPWYc7RdfYshUzUeG7jXPJrAezK
1AMvPApqeO8mJeMv/V0bXlJMpfdiDWiA7kOM81LlhIgfsDLomKWfAgMBAAGjUzBR
MB0GA1UdDgQWBBS/tZSVNYEiqNoEVEF3b2vGpK+KtTAfBgNVHSMEGDAWgBS/tZSV
NYEiqNoEVEF3b2vGpK+KtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4ICAQA0w6P0dCiN2afpVoddkJds0Lg85IffZJjx03mhqWsG2qds7uzsg5qljvWA
8F3JGwy22/N8+43hMUw+qmcqhI2Me1crBvPaa6vh8nvrCiyd7V9OSKqKZ8amObqI
cKTpK8iJl/+K4tzJpdn/RNhYGp6XCIInB7bUu7lyogOdpH+kWVu/SWks1H0Nh59Z
/USiOYa1n7JotN7Fn4MXygDXDAIpyApoiGrrYiCwozlkds6C5lJ6QmlT/PbqwZ2D
0Ruf69vkGe6JSJYHjuOC64Q/FzVmx3uCtG2VJe6Eolmm36vk9GywuUT3SK3/bX7E
EIg3hiQYjEoGJ6XwWGwDGeLp5y7s+OBbHcpodtDwldR/kZUijE3D/AmKCBt16qm7
XgKFE6RXMAxa8khxtQVYh7BxkGGF+ofi98SjsbugA+O2+Z5Ly4AIxx/Ou/IsRmvs
41v03CvCNZ16dNvLWpk1qlD+3oNIxBuFqiUdlS4OQX68XCEWeVNf53KT1CA8qhU9
Jv+5bn5N1+n8WdGXROyPwxvix2hYXQniwnO6ToXdQ4L3wc61TgzSClT9IDF9y9bv
qLUlXM23DepDsJPw8ib32b5XPBzE/7KeHx+LBeLADAi8Dfrzu2aGKq83gIpaQouV
Eh3BrqbwKoKJlQ8Op4aSimkyvzQXyY+ySNkXaqKs8a+ZjdzNjQ==
-----END CERTIFICATE-----
52 changes: 52 additions & 0 deletions client/rest/rest/test/server/certs/restSSL.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
ZLeHTEemDGIzL0gEuV48U/ObVhoo
-----END PRIVATE KEY-----

0 comments on commit af7d147

Please sign in to comment.