forked from tjfontaine/node-ircws
-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
executable file
·227 lines (189 loc) · 6.62 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/env node
'use strict';
var module = require('module');
var net = require('net');
var tls = require('tls');
var util = require('util');
var bunyan = require('bunyan');
var CertCloak = require('./lib/certcloak');
var ConnectStream = require('./lib/connectstream');
var DNSFilter = require('./lib/dnsfilter');
var IRCProxy = require('./lib/ircproxy');
var socketio = require('./lib/sanesocketio');
var Throttle = require('./lib/throttle');
var TorFilter = require('./lib/torfilter');
var config = require('./config');
var logConfig = util._extend({
name: 'webirc',
}, config.loggingConfig);
var LOG = bunyan.createLogger(logConfig);
LOG.addSerializers({
client: function clientSerializer(client) {
return util.format('%d:[%s:%d] (isTor: %s)',
client.clientId,
client.remoteAddress,
client.remotePort,
client.isTor);
},
});
var definedListeners = {};
config.listeners.forEach(function eachListener(listener) {
var proto = undefined;
var listenOptions = {
host: listener.host,
port: listener.port,
};
var serverOptions = {};
var eventName = 'connection';
switch (listener.type) {
case 'plain':
proto = net;
break;
case 'tls':
proto = tls;
serverOptions = listener;
serverOptions.requestCert = true;
serverOptions.rejectUnauthorized = false;
eventName = 'secureConnection';
break;
case 'socketio':
proto = socketio;
serverOptions = listener;
serverOptions.requestCert = true;
serverOptions.rejectUnauthorized = false;
serverOptions.sio_type = 'socketio';
break;
case 'websocket':
proto = socketio;
serverOptions = listener;
serverOptions.requestCert = true;
serverOptions.rejectUnauthorized = false;
serverOptions.sio_type = 'ws';
break;
default:
throw new Error(
'Must define listener type: [plain, tls, websocket, socketio]'
);
}
var listenerKey = listener.host + ':' + listener.port;
definedListeners[listenerKey] = {
listenOptions: listenOptions,
serverOptions: serverOptions,
proto: proto,
eventName: eventName,
};
if (listener.enabled)
enableListener(listenerKey);
});
function enableListener(key) {
var listener = definedListeners[key];
if (!listener) {
LOG.error('trying to enable undefined listener', key);
return;
}
if (listener.server) {
LOG.error('trying to renable enabled listener', key);
return;
}
LOG.info('starting listener', key);
var server = listener.proto.createServer(listener.serverOptions);
server.log = LOG.child({ server: key });
server.listen(listener.listenOptions.port, listener.listenOptions.host);
server.on('listening', function serverListening() {
ConnectStream(server, { eventName: listener.eventName, logger: LOG })
.pipe(Throttle(config, LOG))
.pipe(DNSFilter(config, LOG))
.pipe(CertCloak(config, LOG))
.pipe(TorFilter(config, LOG))
.pipe(IRCProxy(config, LOG))
.resume(); // Don't stop accepting new clients
});
server.on('error', function serverError(err) {
LOG.error('listener failed', err);
// TODO restart listener?
});
listener.server = server;
}
function disableListener(key) {
var listener = definedListeners[key];
if (!listener) {
LOG.error(key, 'trying to disable undefined listener');
return;
}
if (!listener.server) {
LOG.error(key, 'listener already disabled');
return;
}
listener.server.close();
listener.server.removeAllListeners('connection');
listener.server.removeAllListeners('secureConnection');
listener.server = undefined;
}
process.on('SIGHUP', function configReload() {
/*
* This is the worst. Don't do it, the module cache is off limits.
*
* Except, well -- we want to have "real" module semantics so we can define
* some of our config in terms of JavaScript. This means that we need to
* reevaluate the code, but we don't want to use `eval` and a `vm` sandbox is
* not sufficient to avoid runaway configs from polluting the namespace. The
* cleanest thing is to use a child process to evaluate and send back the
* results, but since that involves overhead and other complexity wouldn't it
* be nice if we could just re-require the module outright? In order to do
* that we must invalidate the require cache, but we should *only* invalidate
* the entry for the config itself.
*
* The contract therefore becomes:
* - config should hold no external references
* - config should not create resources that cannot be implicitly collected
* - config evaluation cannot depend on lazy module loading behavior
*
* Holding to this contract, it's reasonably "safe" to muck with the require
* cache. If you're reading this code however, you've probably violated one of
* the contract stipulations, and I'm sorry.
*
* For the other passersby, if you invalidate the entire cache and are relying
* on the "node module as a singleton" -- welp, there goes that. You'll likely
* incur a performance penalty (require is after all synchronous), or
* depending on how often you invalidate the cache, a memory leak.
*
* Best wishes.
*/
var configRealPath = module._resolveFilename('./config');
delete require.cache[configRealPath];
var newConfig = require('./config');
LOG.info('SIGHUP received, reloading config');
try {
newConfig.listeners.forEach(function enableDisable(listener) {
var key = listener.host + ':' + listener.port;
var definedListener = definedListeners[key];
if (!definedListener) {
LOG.error('cannot add new listeners with reload, ignoring', key);
return;
}
if (listener.enabled && definedListener.server &&
('cert' in definedListener.serverOptions) &&
(JSON.stringify(listener.cert) != JSON.stringify(definedListener.serverOptions.cert))) {
LOG.info('certificate for', key, 'changed, reopening');
disableListener(key);
definedListener.serverOptions.key = listener.key;
definedListener.serverOptions.cert = listener.cert;
enableListener(key);
}
else if (listener.enabled && !definedListener.server) {
LOG.info('enabling', key, 'listener');
definedListener.serverOptions.key = listener.key;
definedListener.serverOptions.cert = listener.cert;
enableListener(key);
}
else if (!listener.enabled && definedListener.server) {
LOG.info('disabling', key, 'listener');
disableListener(key);
}
});
config.blockTor = !!newConfig.blockTor;
config.blockTorMessage = newConfig.blockTorMessage;
} catch (e) {
LOG.error(e, 'Failed to parse configuration file');
}
});