From ebaed96fb1aa14c6ac001ed504e2aa070ea3773f Mon Sep 17 00:00:00 2001 From: Kanwar Ujjaval Singh <4216199+kanwarujjaval@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:58:38 +0530 Subject: [PATCH 1/3] add SSL support for HTTP server configuration --- api/api.js | 175 +++++++++++++++++------------- api/config.sample.js | 6 + frontend/express/app.js | 20 +++- frontend/express/config.sample.js | 8 +- 4 files changed, 133 insertions(+), 76 deletions(-) diff --git a/api/api.js b/api/api.js index df712833b8b..843deb94f1b 100644 --- a/api/api.js +++ b/api/api.js @@ -1,4 +1,6 @@ const http = require('http'); +const https = require('https'); +const fs = require('fs'); const cluster = require('cluster'); const formidable = require('formidable'); const os = require('os'); @@ -357,88 +359,113 @@ plugins.connectToAllDatabases().then(function() { plugins.dispatch("/worker", {common: common}); - http.Server((req, res) => { - const params = { - qstring: {}, - res: res, - req: req + const serverOptions = { + port: common.config.api.port, + host: common.config.api.host || '' + }; + + let server; + if (common.config.api.ssl && common.config.api.ssl.enabled) { + const sslOptions = { + key: fs.readFileSync(common.config.api.ssl.key), + cert: fs.readFileSync(common.config.api.ssl.cert) }; + if (common.config.api.ssl.ca) { + sslOptions.ca = fs.readFileSync(common.config.api.ssl.ca); + } + server = https.createServer(sslOptions, handleRequest); + } + else { + server = http.createServer(handleRequest); + } - if (req.method.toLowerCase() === 'post') { - const formidableOptions = {}; - if (countlyConfig.api.maxUploadFileSize) { - formidableOptions.maxFileSize = countlyConfig.api.maxUploadFileSize; - } + server.listen(serverOptions.port, serverOptions.host).timeout = common.config.api.timeout || 120000; + } +}); - const form = new formidable.IncomingForm(formidableOptions); - if (/crash_symbols\/(add_symbol|upload_symbol)/.test(req.url)) { - req.body = []; - req.on('data', (data) => { - req.body.push(data); - }); +/** + * Handle incoming HTTP/HTTPS requests + * @param {http.IncomingMessage} req - The request object + * @param {http.ServerResponse} res - The response object + */ +function handleRequest(req, res) { + const params = { + qstring: {}, + res: res, + req: req + }; + + if (req.method.toLowerCase() === 'post') { + const formidableOptions = {}; + if (countlyConfig.api.maxUploadFileSize) { + formidableOptions.maxFileSize = countlyConfig.api.maxUploadFileSize; + } + + const form = new formidable.IncomingForm(formidableOptions); + if (/crash_symbols\/(add_symbol|upload_symbol)/.test(req.url)) { + req.body = []; + req.on('data', (data) => { + req.body.push(data); + }); + } + else { + req.body = ''; + req.on('data', (data) => { + req.body += data; + }); + } + + let multiFormData = false; + // Check if we have 'multipart/form-data' + if (req.headers['content-type']?.startsWith('multipart/form-data')) { + multiFormData = true; + } + + form.parse(req, (err, fields, files) => { + //handle bakcwards compatability with formiddble v1 + for (let i in files) { + if (files[i].filepath) { + files[i].path = files[i].filepath; } - else { - req.body = ''; - req.on('data', (data) => { - req.body += data; - }); + if (files[i].mimetype) { + files[i].type = files[i].mimetype; } - - let multiFormData = false; - // Check if we have 'multipart/form-data' - if (req.headers['content-type']?.startsWith('multipart/form-data')) { - multiFormData = true; + if (files[i].originalFilename) { + files[i].name = files[i].originalFilename; } - - form.parse(req, (err, fields, files) => { - //handle bakcwards compatability with formiddble v1 - for (let i in files) { - if (files[i].filepath) { - files[i].path = files[i].filepath; - } - if (files[i].mimetype) { - files[i].type = files[i].mimetype; - } - if (files[i].originalFilename) { - files[i].name = files[i].originalFilename; - } - } - params.files = files; - if (multiFormData) { - let formDataUrl = []; - for (const i in fields) { - params.qstring[i] = fields[i]; - formDataUrl.push(`${i}=${fields[i]}`); - } - params.formDataUrl = formDataUrl.join('&'); - } - else { - for (const i in fields) { - params.qstring[i] = fields[i]; - } - } - if (!params.apiPath) { - processRequest(params); - } - }); - } - else if (req.method.toLowerCase() === 'options') { - const headers = {}; - headers["Access-Control-Allow-Origin"] = "*"; - headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS"; - headers["Access-Control-Allow-Headers"] = "countly-token, Content-Type"; - res.writeHead(200, headers); - res.end(); } - //attempt process GET request - else if (req.method.toLowerCase() === 'get') { - processRequest(params); + params.files = files; + if (multiFormData) { + let formDataUrl = []; + for (const i in fields) { + params.qstring[i] = fields[i]; + formDataUrl.push(`${i}=${fields[i]}`); + } + params.formDataUrl = formDataUrl.join('&'); } else { - common.returnMessage(params, 405, "Method not allowed"); + for (const i in fields) { + params.qstring[i] = fields[i]; + } } - }).listen(common.config.api.port, common.config.api.host || '').timeout = common.config.api.timeout || 120000; - - plugins.loadConfigs(common.db); + if (!params.apiPath) { + processRequest(params); + } + }); } -}); + else if (req.method.toLowerCase() === 'options') { + const headers = {}; + headers["Access-Control-Allow-Origin"] = "*"; + headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS"; + headers["Access-Control-Allow-Headers"] = "countly-token, Content-Type"; + res.writeHead(200, headers); + res.end(); + } + //attempt process GET request + else if (req.method.toLowerCase() === 'get') { + processRequest(params); + } + else { + common.returnMessage(params, 405, "Method not allowed"); + } +} diff --git a/api/config.sample.js b/api/config.sample.js index d3532c70d9f..e75bfe7bf11 100644 --- a/api/config.sample.js +++ b/api/config.sample.js @@ -69,6 +69,12 @@ var countlyConfig = { max_sockets: 1024, timeout: 120000, maxUploadFileSize: 200 * 1024 * 1024, // 200MB + ssl: { + enabled: false, + key: "/path/to/ssl/private.key", + cert: "/path/to/ssl/certificate.crt", + ca: "/path/to/ssl/ca_bundle.crt" // Optional: for client certificate verification + } }, /** * Path to use for countly directory, empty path if installed at root of website diff --git a/frontend/express/app.js b/frontend/express/app.js index 9b9b1bd9ece..194ccd85ff6 100644 --- a/frontend/express/app.js +++ b/frontend/express/app.js @@ -34,6 +34,7 @@ var versionInfo = require('./version.info'), COUNTLY_HELPCENTER_LINK = (typeof versionInfo.helpCenterLink === "undefined") ? true : (typeof versionInfo.helpCenterLink === "string") ? versionInfo.helpCenterLink : (typeof versionInfo.helpCenterLink === "boolean") ? versionInfo.helpCenterLink : true, COUNTLY_FEATUREREQUEST_LINK = (typeof versionInfo.featureRequestLink === "undefined") ? true : (typeof versionInfo.featureRequestLink === "string") ? versionInfo.featureRequestLink : (typeof versionInfo.featureRequestLink === "boolean") ? versionInfo.featureRequestLink : true, express = require('express'), + https = require('https'), SkinStore = require('./libs/connect-mongo.js'), expose = require('./libs/express-expose.js'), dollarDefender = require('./libs/dollar-defender.js')({ @@ -1933,5 +1934,22 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_ countlyDb.collection('jobs').createIndex({ name: 1 }, function() {}); countlyDb.collection('long_tasks').createIndex({ manually_create: 1, start: -1 }, function() {}); - app.listen(countlyConfig.web.port, countlyConfig.web.host || ''); + const serverOptions = { + port: countlyConfig.web.port, + host: countlyConfig.web.host || '' + }; + + if (countlyConfig.web.ssl && countlyConfig.web.ssl.enabled) { + const sslOptions = { + key: fs.readFileSync(countlyConfig.web.ssl.key), + cert: fs.readFileSync(countlyConfig.web.ssl.cert) + }; + if (countlyConfig.web.ssl.ca) { + sslOptions.ca = fs.readFileSync(countlyConfig.web.ssl.ca); + } + https.createServer(sslOptions, app).listen(serverOptions.port, serverOptions.host); + } + else { + app.listen(serverOptions.port, serverOptions.host); + } }); diff --git a/frontend/express/config.sample.js b/frontend/express/config.sample.js index dbc1f9e8e72..9907819d90c 100644 --- a/frontend/express/config.sample.js +++ b/frontend/express/config.sample.js @@ -77,7 +77,13 @@ var countlyConfig = { track: "all", theme: "", session_secret: "countlyss", - session_name: "connect.sid" + session_name: "connect.sid", + ssl: { + enabled: false, + key: "/path/to/ssl/private.key", + cert: "/path/to/ssl/certificate.crt", + ca: "/path/to/ssl/ca_bundle.crt" // Optional: for client certificate verification + } }, /** * Cookie configuration From 7dd0caa47b157370ababe3a211629059e9aabc14 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 18 Jun 2025 12:05:34 +0700 Subject: [PATCH 2/3] Add nginx config for internal ssl setup --- bin/config/nginx.server.internal_ssl.conf | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 bin/config/nginx.server.internal_ssl.conf diff --git a/bin/config/nginx.server.internal_ssl.conf b/bin/config/nginx.server.internal_ssl.conf new file mode 100644 index 00000000000..cd7a8e1cd79 --- /dev/null +++ b/bin/config/nginx.server.internal_ssl.conf @@ -0,0 +1,120 @@ +server { + listen 80; + listen [::]:80 ipv6only=on; + server_name localhost; + + access_log off; + + rewrite ^ https://$host$request_uri? permanent; +} + +# HTTPS configuration + +server { + listen 443; + listen [::]:443 ipv6only=on; + server_name localhost; + + access_log off; + + ssl on; + + # support only known-secure cryptographic protocols + # SSLv3 is broken by POODLE as of October 2014 + ssl_protocols TLSv1.2 TLSv1.3; + + # make the server choose the best cipher instead of the browser + # Perfect Forward Secrecy(PFS) is frequently compromised without this + ssl_prefer_server_ciphers on; + + # support only believed secure ciphersuites using the following priority: + # 1.) prefer PFS enabled ciphers + # 2.) prefer AES128 over AES256 for speed (AES128 has completely adequate security for now) + # 3.) Support DES3 for IE8 support + # + # disable the following ciphersuites completely + # 1.) null ciphers + # 2.) ciphers with low security + # 3.) fixed ECDH cipher (does not allow for PFS) + # 4.) known vulnerable cypers (MD5, RC4, etc) + # 5.) little-used ciphers (Camellia, Seed) + 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'; + + # Cache SSL Sessions for up to 10 minutes + # This improves performance by avoiding the costly session negotiation process where possible + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # allow Nginx to send OCSP results during the connection process + ssl_stapling on; + + # Use 2048 bit Diffie-Hellman RSA key parameters + # (otherwise Nginx defaults to 1024 bit, lowering the strength of encryption # when using PFS) + # Generated by OpenSSL with the following command: + # openssl dhparam -outform pem -out /etc/nginx/ssl/dhparam2048.pem 2048 + ssl_dhparam /path/to/dhparams.pem; + + # Provide path to certificates and keys + ssl_certificate /path/to/certificate-bundle.crt; + ssl_certificate_key /path/to/certificate-key.key; + ssl_trusted_certificate /path/to/chain.pem; + + location = /i { + if ($http_content_type = "text/ping") { + return 404; + } + # countly server is running with ssl, so use https here + proxy_pass https://localhost:3001; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + # if countly server is using self-signed certificate, this will disable certificate verification + proxy_ssl_verify off; + } + + location ^~ /i/ { + if ($http_content_type = "text/ping") { + return 404; + } + # countly server is running with ssl, so use https here + proxy_pass https://localhost:3001; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + # if countly server is using self-signed certificate, this will disable certificate verification + proxy_ssl_verify off; + } + + location = /o { + if ($http_content_type = "text/ping") { + return 404; + } + # countly server is running with ssl, so use https here + proxy_pass https://localhost:3001; + # if countly server is using self-signed certificate, this will disable certificate verification + proxy_ssl_verify off; + } + + location ^~ /o/ { + if ($http_content_type = "text/ping") { + return 404; + } + # countly server is running with ssl, so use https here + proxy_pass https://localhost:3001; + # if countly server is using self-signed certificate, this will disable certificate verification + proxy_ssl_verify off; + } + + location / { + if ($http_content_type = "text/ping") { + return 404; + } + # countly server is running with ssl, so use https here + proxy_pass https://localhost:6001; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + # if countly server is using self-signed certificate, this will disable certificate verification + proxy_ssl_verify off; + } +} + From ff05047b65b5774d46a2030948e3c3f23e158134 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 18 Jun 2025 12:35:27 +0700 Subject: [PATCH 3/3] Update config extender for ssl support --- api/configextender.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/configextender.js b/api/configextender.js index 670c736c24d..bc97dce5eeb 100644 --- a/api/configextender.js +++ b/api/configextender.js @@ -32,14 +32,26 @@ const OVERRIDES = { API: { MAX_SOCKETS: 'max_sockets', - MAX_UPLOAD_FILE_SIZE: 'maxUploadFileSize' + MAX_UPLOAD_FILE_SIZE: 'maxUploadFileSize', + SSL: { + ENABLED: 'enabled', + KEY: 'key', + CERT: 'cert', + CA: 'ca', + }, }, WEB: { USE_INTERCOM: 'use_intercom', SECURE_COOKIES: 'secure_cookies', SESSION_SECRET: 'session_secret', - SESSION_NAME: 'session_name' + SESSION_NAME: 'session_name', + SSL: { + ENABLED: 'enabled', + KEY: 'key', + CERT: 'cert', + CA: 'ca', + }, }, MAIL: {