Skip to content

Commit 741ba65

Browse files
Merge pull request #52 from project-sunbird/cts-vul-fixes
Vulnerability & Code Quality Fixes
2 parents d46e9b2 + 21ab7f8 commit 741ba65

File tree

11 files changed

+1091
-1133
lines changed

11 files changed

+1091
-1133
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules
22
.nyc_output
33
coverage
44
.audit.json
5+
*-audit.json
56
telemetry-*.log
67
mochawesome-report
78
*.log

src/app.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const express = require('express'),
22
cluster = require('express-cluster'),
33
cookieParser = require('cookie-parser'),
4+
helmet = require('helmet'),
45
logger = require('morgan'),
56
bodyParser = require('body-parser'),
67
envVariables = require('./envVariables'),
@@ -9,12 +10,15 @@ const express = require('express'),
910

1011
const createAppServer = () => {
1112
const app = express();
13+
app.use(helmet());
1214
app.use((req, res, next) => {
13-
res.header('Access-Control-Allow-Origin', '*');
15+
res.header('Access-Control-Allow-Origin', envVariables.allowedOrigins);
1416
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,PATCH,DELETE,OPTIONS');
15-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization,' + 'cid, user-id, x-auth, Cache-Control, X-Requested-With, datatype, *');
16-
if (req.method === 'OPTIONS') res.sendStatus(200);
17-
else next();
17+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, cid, user-id, x-auth, Cache-Control, X-Requested-With, datatype, *');
18+
if (req.method === 'OPTIONS') {
19+
return res.status(200).end();
20+
}
21+
next();
1822
});
1923
app.use(bodyParser.json({ limit: '5mb' }));
2024
app.use(logger('dev'));

src/dispatcher/dispatcher.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
const winston = require('winston');
2-
require('winston-daily-rotate-file');
3-
require('./kafka-dispatcher');
4-
require('./cassandra-dispatcher');
52

63
const defaultFileOptions = {
74
filename: 'dispatcher-%DATE%.log',
@@ -15,33 +12,55 @@ const defaultFileOptions = {
1512
class Dispatcher {
1613
constructor(options) {
1714
if (!options) throw new Error('Dispatcher options are required');
18-
this.logger = new(winston.Logger)({level: 'info'});
1915
this.options = options;
20-
if (this.options.dispatcher == 'kafka') {
21-
this.logger.add(winston.transports.Kafka, this.options);
16+
this.transport = null;
17+
18+
if (this.options.dispatcher === 'kafka') {
19+
const { KafkaDispatcher } = require('./kafka-dispatcher');
20+
this.transport = new KafkaDispatcher(this.options);
2221
console.log('Kafka transport enabled !!!');
23-
} else if (this.options.dispatcher == 'file') {
22+
} else if (this.options.dispatcher === 'file') {
23+
require('winston-daily-rotate-file');
2424
const config = Object.assign(defaultFileOptions, this.options);
25-
this.logger.add(winston.transports.DailyRotateFile, config);
25+
this.transport = new winston.transports.DailyRotateFile(config);
2626
console.log('File transport enabled !!!');
2727
} else if (this.options.dispatcher === 'cassandra') {
28-
this.logger.add(winston.transports.Cassandra, this.options);
28+
require('./cassandra-dispatcher');
29+
this.transport = new winston.transports.Cassandra(this.options);
2930
console.log('Cassandra transport enabled !!!');
3031
} else { // Log to console
3132
this.options.dispatcher = 'console';
3233
const config = Object.assign({json: true,stringify: (obj) => JSON.stringify(obj)}, this.options);
33-
this.logger.add(winston.transports.Console, config);
34+
this.transport = new winston.transports.Console(config);
3435
console.log('Console transport enabled !!!');
3536
}
37+
38+
this.logger = winston.createLogger({
39+
level: 'info',
40+
transports: [this.transport]
41+
});
3642
}
3743

3844
dispatch(mid, message, callback) {
39-
this.logger.log('info', message, {mid: mid}, callback);
45+
// Winston 3.x logger.log doesn't support callbacks, but individual transports do
46+
// We call the transport's log method directly to get proper callback support
47+
const info = {
48+
level: 'info',
49+
message: message,
50+
mid: mid
51+
};
52+
53+
// Call the transport's log method directly with callback
54+
this.transport.log(info, (err) => {
55+
if (callback) {
56+
callback(err);
57+
}
58+
});
4059
}
4160

4261
health(callback) {
4362
if (this.options.dispatcher === 'kafka') {
44-
this.logger.transports['kafka'].health(callback);
63+
this.transport.health(callback);
4564
} else if (this.options.dispatcher === 'console') {
4665
callback(true);
4766
} else { // need to add health method for file/cassandra

src/dispatcher/kafka-dispatcher.js

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,125 @@
1-
const winston = require('winston'),
2-
kafka = require('kafka-node'),
1+
const Transport = require('winston-transport'),
32
_ = require('lodash'),
4-
HighLevelProducer = kafka.HighLevelProducer,
3+
{ Kafka, CompressionTypes } = require('kafkajs'),
4+
config = require('../envVariables'),
55
defaultOptions = {
66
kafkaHost: 'localhost:9092',
77
maxAsyncRequests: 100,
88
topic: 'local.ingestion',
99
compression_type: 'none'
1010
};
1111

12-
class KafkaDispatcher extends winston.Transport {
12+
function mapCompressionAttr(attr) {
13+
// kafka-node used numeric attributes: 0 = none, 1 = gzip, 2 = snappy
14+
if (attr === 2) return CompressionTypes.Snappy;
15+
if (attr === 1) return CompressionTypes.GZIP;
16+
return CompressionTypes.None;
17+
}
18+
19+
class KafkaDispatcher extends Transport {
1320
constructor(options) {
14-
super();
21+
super(options);
1522
this.name = 'kafka';
1623
this.options = _.assignInWith(defaultOptions, options, (objValue, srcValue) => srcValue ? srcValue : objValue);
17-
if (this.options.compression_type == 'snappy') {
24+
if (this.options.compression_type === 'snappy') {
1825
this.compression_attribute = 2;
19-
} else if(this.options.compression_type == 'gzip') {
26+
} else if(this.options.compression_type === 'gzip') {
2027
this.compression_attribute = 1;
2128
} else {
2229
this.compression_attribute = 0;
2330
}
24-
this.client = new kafka.KafkaClient({
25-
kafkaHost: this.options.kafkaHost,
26-
maxAsyncRequests: this.options.maxAsyncRequests
27-
});
28-
this.producer = new HighLevelProducer(this.client);
29-
this.producer.on('ready', () => console.log('kafka dispatcher is ready'));
30-
this.producer.on('error', (err) => console.error('Unable to connect to kafka', err));
31+
32+
// kafkajs expects an array of broker strings
33+
const brokers = (typeof this.options.kafkaHost === 'string') ? [this.options.kafkaHost] : this.options.kafkaHost;
34+
this._kafka = new Kafka({ brokers });
35+
this._producer = this._kafka.producer();
36+
this._admin = this._kafka.admin();
37+
this._producerConnected = false;
38+
39+
// Backwards-compatible lightweight wrappers so existing code/tests that
40+
// expect producer.send(payloads, cb) and client.topicExists(topic, cb)
41+
// continue to work.
42+
this.producer = {
43+
send: (payloads, cb) => {
44+
// payloads is an array like [{ topic, key, messages, attributes, partition }]
45+
const topicMessages = payloads.map(p => {
46+
const msg = { key: p.key, value: p.messages };
47+
if (p.hasOwnProperty('partition')) msg.partition = p.partition;
48+
return {
49+
topic: p.topic,
50+
messages: [msg],
51+
compression: mapCompressionAttr(p.attributes)
52+
};
53+
});
54+
55+
// ensure producer is connected, then send batch
56+
const sendPromise = this._producerConnected
57+
? Promise.resolve()
58+
: this._producer.connect().then(() => { this._producerConnected = true; });
59+
60+
sendPromise
61+
.then(() => this._producer.sendBatch({ topicMessages }))
62+
.then(() => { if (cb) cb(); })
63+
.catch(err => { if (cb) cb(err); });
64+
}
65+
};
66+
67+
this.client = {
68+
topicExists: (topic, cb) => {
69+
// kafkajs admin.fetchTopicMetadata throws if topic doesn't exist
70+
this._admin.connect()
71+
.then(() => this._admin.fetchTopicMetadata({ topics: [topic] }))
72+
.then(() => this._admin.disconnect())
73+
.then(() => cb && cb(null))
74+
.catch(err => {
75+
// ensure disconnect
76+
this._admin.disconnect().catch(() => {});
77+
cb && cb(err);
78+
});
79+
}
80+
};
81+
82+
// log basic connection info asynchronously
83+
this._producer.connect()
84+
.then(() => {
85+
this._producerConnected = true;
86+
console.log('kafka dispatcher producer connected');
87+
})
88+
.catch(err => console.error('Unable to connect kafka producer', err));
89+
this._admin.connect()
90+
.then(() => this._admin.disconnect())
91+
.catch(() => {});
3192
}
32-
log(level, msg, meta, callback) {
93+
94+
log(info, callback) {
95+
// Modern winston 3.x transport signature: log(info, callback)
96+
// info contains: level, message, and other metadata
97+
// msg/message is expected to be a JSON string. Inject a top-level dataset key
98+
// from configuration if provided and not already present.
99+
const msg = info.message;
100+
let outgoing = msg;
101+
try {
102+
if (typeof msg === 'string') {
103+
const parsed = JSON.parse(msg);
104+
if (parsed && typeof parsed === 'object') {
105+
if (config.dataset && !parsed.hasOwnProperty('dataset')) {
106+
parsed.dataset = config.dataset;
107+
}
108+
outgoing = JSON.stringify(parsed);
109+
}
110+
}
111+
} catch (e) {
112+
// if parsing fails, leave the message as-is
113+
}
114+
33115
this.producer.send([{
34116
topic: this.options.topic,
35-
key: meta.mid,
36-
messages: msg,
117+
key: info.mid,
118+
messages: outgoing,
37119
attributes: this.compression_attribute
38120
}], callback);
39121
}
122+
40123
health(callback) {
41124
this.client.topicExists(this.options.topic, (err) => {
42125
if (err) callback(false);
@@ -45,6 +128,4 @@ class KafkaDispatcher extends winston.Transport {
45128
}
46129
}
47130

48-
winston.transports.Kafka = KafkaDispatcher;
49-
50131
module.exports = { KafkaDispatcher };

src/envVariables.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const envVariables = {
1919
contactPoints: (process.env.telemetry_cassandra_contactpoints || 'localhost').split(','),
2020
cassandraTtl: process.env.telemetry_cassandra_ttl,
2121
port: process.env.telemetry_service_port || 9001,
22-
threads: process.env.telemetry_service_threads || os.cpus().length
22+
threads: process.env.telemetry_service_threads || os.cpus().length,
23+
// dataset to be injected into outgoing telemetry events
24+
dataset: process.env.telemetry_dataset || 'sb-telemetry',
25+
// CORS configuration
26+
allowedOrigins: process.env.telemetry_allowed_origins || '*'
2327
};
2428
module.exports = envVariables;

0 commit comments

Comments
 (0)