Skip to content

Commit ed4638a

Browse files
authored
Merge pull request #6295 from Countly/feat/ssl-support
[experimental] add SSL support for HTTP server configuration
2 parents 3ea5fed + c60f466 commit ed4638a

File tree

6 files changed

+267
-78
lines changed

6 files changed

+267
-78
lines changed

api/api.js

Lines changed: 101 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const http = require('http');
2+
const https = require('https');
3+
const fs = require('fs');
24
const cluster = require('cluster');
35
const formidable = require('formidable');
46
const os = require('os');
@@ -357,88 +359,113 @@ plugins.connectToAllDatabases().then(function() {
357359

358360
plugins.dispatch("/worker", {common: common});
359361

360-
http.Server((req, res) => {
361-
const params = {
362-
qstring: {},
363-
res: res,
364-
req: req
362+
const serverOptions = {
363+
port: common.config.api.port,
364+
host: common.config.api.host || ''
365+
};
366+
367+
let server;
368+
if (common.config.api.ssl && common.config.api.ssl.enabled) {
369+
const sslOptions = {
370+
key: fs.readFileSync(common.config.api.ssl.key),
371+
cert: fs.readFileSync(common.config.api.ssl.cert)
365372
};
373+
if (common.config.api.ssl.ca) {
374+
sslOptions.ca = fs.readFileSync(common.config.api.ssl.ca);
375+
}
376+
server = https.createServer(sslOptions, handleRequest);
377+
}
378+
else {
379+
server = http.createServer(handleRequest);
380+
}
366381

367-
if (req.method.toLowerCase() === 'post') {
368-
const formidableOptions = {};
369-
if (countlyConfig.api.maxUploadFileSize) {
370-
formidableOptions.maxFileSize = countlyConfig.api.maxUploadFileSize;
371-
}
382+
server.listen(serverOptions.port, serverOptions.host).timeout = common.config.api.timeout || 120000;
383+
}
384+
});
372385

373-
const form = new formidable.IncomingForm(formidableOptions);
374-
if (/crash_symbols\/(add_symbol|upload_symbol)/.test(req.url)) {
375-
req.body = [];
376-
req.on('data', (data) => {
377-
req.body.push(data);
378-
});
386+
/**
387+
* Handle incoming HTTP/HTTPS requests
388+
* @param {http.IncomingMessage} req - The request object
389+
* @param {http.ServerResponse} res - The response object
390+
*/
391+
function handleRequest(req, res) {
392+
const params = {
393+
qstring: {},
394+
res: res,
395+
req: req
396+
};
397+
398+
if (req.method.toLowerCase() === 'post') {
399+
const formidableOptions = {};
400+
if (countlyConfig.api.maxUploadFileSize) {
401+
formidableOptions.maxFileSize = countlyConfig.api.maxUploadFileSize;
402+
}
403+
404+
const form = new formidable.IncomingForm(formidableOptions);
405+
if (/crash_symbols\/(add_symbol|upload_symbol)/.test(req.url)) {
406+
req.body = [];
407+
req.on('data', (data) => {
408+
req.body.push(data);
409+
});
410+
}
411+
else {
412+
req.body = '';
413+
req.on('data', (data) => {
414+
req.body += data;
415+
});
416+
}
417+
418+
let multiFormData = false;
419+
// Check if we have 'multipart/form-data'
420+
if (req.headers['content-type']?.startsWith('multipart/form-data')) {
421+
multiFormData = true;
422+
}
423+
424+
form.parse(req, (err, fields, files) => {
425+
//handle bakcwards compatability with formiddble v1
426+
for (let i in files) {
427+
if (files[i].filepath) {
428+
files[i].path = files[i].filepath;
379429
}
380-
else {
381-
req.body = '';
382-
req.on('data', (data) => {
383-
req.body += data;
384-
});
430+
if (files[i].mimetype) {
431+
files[i].type = files[i].mimetype;
385432
}
386-
387-
let multiFormData = false;
388-
// Check if we have 'multipart/form-data'
389-
if (req.headers['content-type']?.startsWith('multipart/form-data')) {
390-
multiFormData = true;
433+
if (files[i].originalFilename) {
434+
files[i].name = files[i].originalFilename;
391435
}
392-
393-
form.parse(req, (err, fields, files) => {
394-
//handle bakcwards compatability with formiddble v1
395-
for (let i in files) {
396-
if (files[i].filepath) {
397-
files[i].path = files[i].filepath;
398-
}
399-
if (files[i].mimetype) {
400-
files[i].type = files[i].mimetype;
401-
}
402-
if (files[i].originalFilename) {
403-
files[i].name = files[i].originalFilename;
404-
}
405-
}
406-
params.files = files;
407-
if (multiFormData) {
408-
let formDataUrl = [];
409-
for (const i in fields) {
410-
params.qstring[i] = fields[i];
411-
formDataUrl.push(`${i}=${fields[i]}`);
412-
}
413-
params.formDataUrl = formDataUrl.join('&');
414-
}
415-
else {
416-
for (const i in fields) {
417-
params.qstring[i] = fields[i];
418-
}
419-
}
420-
if (!params.apiPath) {
421-
processRequest(params);
422-
}
423-
});
424-
}
425-
else if (req.method.toLowerCase() === 'options') {
426-
const headers = {};
427-
headers["Access-Control-Allow-Origin"] = "*";
428-
headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS";
429-
headers["Access-Control-Allow-Headers"] = "countly-token, Content-Type";
430-
res.writeHead(200, headers);
431-
res.end();
432436
}
433-
//attempt process GET request
434-
else if (req.method.toLowerCase() === 'get') {
435-
processRequest(params);
437+
params.files = files;
438+
if (multiFormData) {
439+
let formDataUrl = [];
440+
for (const i in fields) {
441+
params.qstring[i] = fields[i];
442+
formDataUrl.push(`${i}=${fields[i]}`);
443+
}
444+
params.formDataUrl = formDataUrl.join('&');
436445
}
437446
else {
438-
common.returnMessage(params, 405, "Method not allowed");
447+
for (const i in fields) {
448+
params.qstring[i] = fields[i];
449+
}
439450
}
440-
}).listen(common.config.api.port, common.config.api.host || '').timeout = common.config.api.timeout || 120000;
441-
442-
plugins.loadConfigs(common.db);
451+
if (!params.apiPath) {
452+
processRequest(params);
453+
}
454+
});
443455
}
444-
});
456+
else if (req.method.toLowerCase() === 'options') {
457+
const headers = {};
458+
headers["Access-Control-Allow-Origin"] = "*";
459+
headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS";
460+
headers["Access-Control-Allow-Headers"] = "countly-token, Content-Type";
461+
res.writeHead(200, headers);
462+
res.end();
463+
}
464+
//attempt process GET request
465+
else if (req.method.toLowerCase() === 'get') {
466+
processRequest(params);
467+
}
468+
else {
469+
common.returnMessage(params, 405, "Method not allowed");
470+
}
471+
}

api/config.sample.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ var countlyConfig = {
6969
max_sockets: 1024,
7070
timeout: 120000,
7171
maxUploadFileSize: 200 * 1024 * 1024, // 200MB
72+
ssl: {
73+
enabled: false,
74+
key: "/path/to/ssl/private.key",
75+
cert: "/path/to/ssl/certificate.crt",
76+
ca: "/path/to/ssl/ca_bundle.crt" // Optional: for client certificate verification
77+
}
7278
},
7379
/**
7480
* Path to use for countly directory, empty path if installed at root of website

api/configextender.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,26 @@ const OVERRIDES = {
3232

3333
API: {
3434
MAX_SOCKETS: 'max_sockets',
35-
MAX_UPLOAD_FILE_SIZE: 'maxUploadFileSize'
35+
MAX_UPLOAD_FILE_SIZE: 'maxUploadFileSize',
36+
SSL: {
37+
ENABLED: 'enabled',
38+
KEY: 'key',
39+
CERT: 'cert',
40+
CA: 'ca',
41+
},
3642
},
3743

3844
WEB: {
3945
USE_INTERCOM: 'use_intercom',
4046
SECURE_COOKIES: 'secure_cookies',
4147
SESSION_SECRET: 'session_secret',
42-
SESSION_NAME: 'session_name'
48+
SESSION_NAME: 'session_name',
49+
SSL: {
50+
ENABLED: 'enabled',
51+
KEY: 'key',
52+
CERT: 'cert',
53+
CA: 'ca',
54+
},
4355
},
4456

4557
MAIL: {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
server {
2+
listen 80;
3+
listen [::]:80 ipv6only=on;
4+
server_name localhost;
5+
6+
access_log off;
7+
8+
rewrite ^ https://$host$request_uri? permanent;
9+
}
10+
11+
# HTTPS configuration
12+
13+
server {
14+
listen 443;
15+
listen [::]:443 ipv6only=on;
16+
server_name localhost;
17+
18+
access_log off;
19+
20+
ssl on;
21+
22+
# support only known-secure cryptographic protocols
23+
# SSLv3 is broken by POODLE as of October 2014
24+
ssl_protocols TLSv1.2 TLSv1.3;
25+
26+
# make the server choose the best cipher instead of the browser
27+
# Perfect Forward Secrecy(PFS) is frequently compromised without this
28+
ssl_prefer_server_ciphers on;
29+
30+
# support only believed secure ciphersuites using the following priority:
31+
# 1.) prefer PFS enabled ciphers
32+
# 2.) prefer AES128 over AES256 for speed (AES128 has completely adequate security for now)
33+
# 3.) Support DES3 for IE8 support
34+
#
35+
# disable the following ciphersuites completely
36+
# 1.) null ciphers
37+
# 2.) ciphers with low security
38+
# 3.) fixed ECDH cipher (does not allow for PFS)
39+
# 4.) known vulnerable cypers (MD5, RC4, etc)
40+
# 5.) little-used ciphers (Camellia, Seed)
41+
ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 +SHA !DES-CBC3-SHA !aNULL !eNULL !LOW !kECDH !DSS !3DES !MD5 !EXP !PSK !SRP !CAMELLIA !SEED';
42+
43+
# Cache SSL Sessions for up to 10 minutes
44+
# This improves performance by avoiding the costly session negotiation process where possible
45+
ssl_session_cache shared:SSL:50m;
46+
ssl_session_timeout 1d;
47+
ssl_session_tickets off;
48+
49+
# allow Nginx to send OCSP results during the connection process
50+
ssl_stapling on;
51+
52+
# Use 2048 bit Diffie-Hellman RSA key parameters
53+
# (otherwise Nginx defaults to 1024 bit, lowering the strength of encryption # when using PFS)
54+
# Generated by OpenSSL with the following command:
55+
# openssl dhparam -outform pem -out /etc/nginx/ssl/dhparam2048.pem 2048
56+
ssl_dhparam /path/to/dhparams.pem;
57+
58+
# Provide path to certificates and keys
59+
ssl_certificate /path/to/certificate-bundle.crt;
60+
ssl_certificate_key /path/to/certificate-key.key;
61+
ssl_trusted_certificate /path/to/chain.pem;
62+
63+
location = /i {
64+
if ($http_content_type = "text/ping") {
65+
return 404;
66+
}
67+
# countly server is running with ssl, so use https here
68+
proxy_pass https://localhost:3001;
69+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
70+
proxy_set_header X-Real-IP $remote_addr;
71+
# if countly server is using self-signed certificate, this will disable certificate verification
72+
proxy_ssl_verify off;
73+
}
74+
75+
location ^~ /i/ {
76+
if ($http_content_type = "text/ping") {
77+
return 404;
78+
}
79+
# countly server is running with ssl, so use https here
80+
proxy_pass https://localhost:3001;
81+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
82+
proxy_set_header X-Real-IP $remote_addr;
83+
# if countly server is using self-signed certificate, this will disable certificate verification
84+
proxy_ssl_verify off;
85+
}
86+
87+
location = /o {
88+
if ($http_content_type = "text/ping") {
89+
return 404;
90+
}
91+
# countly server is running with ssl, so use https here
92+
proxy_pass https://localhost:3001;
93+
# if countly server is using self-signed certificate, this will disable certificate verification
94+
proxy_ssl_verify off;
95+
}
96+
97+
location ^~ /o/ {
98+
if ($http_content_type = "text/ping") {
99+
return 404;
100+
}
101+
# countly server is running with ssl, so use https here
102+
proxy_pass https://localhost:3001;
103+
# if countly server is using self-signed certificate, this will disable certificate verification
104+
proxy_ssl_verify off;
105+
}
106+
107+
location / {
108+
if ($http_content_type = "text/ping") {
109+
return 404;
110+
}
111+
# countly server is running with ssl, so use https here
112+
proxy_pass https://localhost:6001;
113+
proxy_set_header Host $http_host;
114+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
115+
proxy_set_header X-Real-IP $remote_addr;
116+
# if countly server is using self-signed certificate, this will disable certificate verification
117+
proxy_ssl_verify off;
118+
}
119+
}
120+

frontend/express/app.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var versionInfo = require('./version.info'),
3434
COUNTLY_HELPCENTER_LINK = (typeof versionInfo.helpCenterLink === "undefined") ? true : (typeof versionInfo.helpCenterLink === "string") ? versionInfo.helpCenterLink : (typeof versionInfo.helpCenterLink === "boolean") ? versionInfo.helpCenterLink : true,
3535
COUNTLY_FEATUREREQUEST_LINK = (typeof versionInfo.featureRequestLink === "undefined") ? true : (typeof versionInfo.featureRequestLink === "string") ? versionInfo.featureRequestLink : (typeof versionInfo.featureRequestLink === "boolean") ? versionInfo.featureRequestLink : true,
3636
express = require('express'),
37+
https = require('https'),
3738
SkinStore = require('./libs/connect-mongo.js'),
3839
expose = require('./libs/express-expose.js'),
3940
dollarDefender = require('./libs/dollar-defender.js')({
@@ -1933,5 +1934,22 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
19331934
countlyDb.collection('jobs').createIndex({ name: 1 }, function() {});
19341935
countlyDb.collection('long_tasks').createIndex({ manually_create: 1, start: -1 }, function() {});
19351936

1936-
app.listen(countlyConfig.web.port, countlyConfig.web.host || '');
1937+
const serverOptions = {
1938+
port: countlyConfig.web.port,
1939+
host: countlyConfig.web.host || ''
1940+
};
1941+
1942+
if (countlyConfig.web.ssl && countlyConfig.web.ssl.enabled) {
1943+
const sslOptions = {
1944+
key: fs.readFileSync(countlyConfig.web.ssl.key),
1945+
cert: fs.readFileSync(countlyConfig.web.ssl.cert)
1946+
};
1947+
if (countlyConfig.web.ssl.ca) {
1948+
sslOptions.ca = fs.readFileSync(countlyConfig.web.ssl.ca);
1949+
}
1950+
https.createServer(sslOptions, app).listen(serverOptions.port, serverOptions.host);
1951+
}
1952+
else {
1953+
app.listen(serverOptions.port, serverOptions.host);
1954+
}
19371955
});

0 commit comments

Comments
 (0)