tlsd 2.16.3 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlsd",
3
- "version": "2.16.3",
3
+ "version": "2.18.0",
4
4
  "description": "A server for web app prototyping with HTTPS and Websockets",
5
5
  "main": "tlsd.js",
6
6
  "bin": {
@@ -36,11 +36,16 @@
36
36
  .textarea-group:first-child {
37
37
  position: relative;
38
38
  }
39
+
40
+ .textarea-group-head {
41
+ display: flex;
42
+ justify-content: space-between;
43
+ }
39
44
 
40
45
  .send-button {
41
- position: absolute;
42
- top: 0;
43
- right: 0;
46
+ zposition: absolute;
47
+ ztop: 0;
48
+ zright: 0;
44
49
  background-color: #007bff;
45
50
  color: white;
46
51
  border: none;
@@ -94,8 +99,13 @@
94
99
  <body>
95
100
  <div class="grid-container">
96
101
  <div class="textarea-group">
97
- <label class="textarea-label">Message Data</label>
98
- <button class="send-button">Send</button>
102
+ <div class="textarea-group-head">
103
+ <label class="textarea-label">Message Data</label>
104
+ <div>
105
+ <button class="send-button" id="send_auto">Send</button>
106
+ <button class="send-button" id="send_post">POST</button>
107
+ </div>
108
+ </div>
99
109
  <textarea class="textarea-input" id="e_input" ></textarea>
100
110
  </div>
101
111
  <div class="textarea-group">
@@ -115,14 +125,15 @@
115
125
  <script>
116
126
 
117
127
  // Get the send button and first textarea
128
+ const sendButtonAuto = document.querySelector('#send_auto');
129
+ const sendButtonPOST = document.querySelector('#send_post');
118
130
  const sendButton = document.querySelector('.send-button');
119
131
  const e_input = document.querySelector('#e_input');
120
132
  const e_preview = document.querySelector('#e_preview');
121
133
  const e_sent = document.querySelector('#e_sent');
122
134
  const e_received = document.querySelector('#e_received');
123
-
124
- // Add click event listener to the send button
125
- sendButton.addEventListener('click', function() {
135
+
136
+ function send( post = false ) {
126
137
  // Get the content from the first textarea
127
138
  const textareaContent = e_input.value;
128
139
 
@@ -139,13 +150,22 @@
139
150
 
140
151
  e_received.value = "...";
141
152
 
142
- RPC( data, function( rsp ) {
153
+ let fn = post ? RPC.POST : RPC;
154
+
155
+ fn( data, function( rsp ) {
143
156
  e_received.value = o2j( rsp, null, 2 );
144
157
  }, function( err ) {
145
158
  e_received.value = "ERROR:\n" + o2j( err, null, 2 );
146
159
  });
147
-
160
+ }
161
+
162
+ sendButtonAuto.addEventListener('click', function() {
163
+ send( false );
148
164
  });
165
+ sendButtonPOST.addEventListener('click', function() {
166
+ send( true );
167
+ });
168
+
149
169
 
150
170
  // Load content from local storage when page loads
151
171
  window.addEventListener('pageshow', function() {
@@ -23,6 +23,11 @@
23
23
  const o2j = function( v ) { try { return JSON.stringify( v, null, 2 ) } catch( e ) { return null } }
24
24
  const time = function( dt ) { return Math.round( ( new Date() ).getTime() / 1000 ); }
25
25
 
26
+ const getCookie = function( name ) {
27
+ const match = document.cookie.match( new RegExp( "(^| )" + name + "=([^;]+)" ) );
28
+ return match ? match[ 2 ] : null;
29
+ };
30
+
26
31
 
27
32
  let DBG = function( ...args ) {
28
33
  if( RPC.debug ) {
@@ -238,6 +243,10 @@
238
243
  xhr.open( "POST", RPC_URL );
239
244
  xhr.setRequestHeader( "Content-Type", "application/json" );
240
245
  xhr.setRequestHeader( "Accept", "application/json" );
246
+ const csrfToken = getCookie( "csrf_token" );
247
+ if( csrfToken ) {
248
+ xhr.setRequestHeader( "X-CSRF-Token", csrfToken );
249
+ }
241
250
  xhr.send( o2j( msg ) );
242
251
  };
243
252
 
package/tlsd.js CHANGED
@@ -10,7 +10,6 @@ const serveStatic = require( "serve-static" );
10
10
 
11
11
  const body_parser = require( "body-parser" ).json( { limit: "10mb" } );
12
12
  const compression = require( "compression" )();
13
- const cors = require( "cors" )();
14
13
  const queryString = require( "querystring" );
15
14
 
16
15
  const { log5, o2j, j2o, toInt, is_dir, sha1 } = require( "sleepless" );
@@ -21,8 +20,40 @@ const { D, V, I, W, E } = L;
21
20
  const UPLOAD_DIR = process.env.UPLOAD_DIR || "/tmp/tlsd-put-files/";
22
21
  const UPLOAD_MAX_BYTES = toInt( process.env.UPLOAD_MAX_BYTES ) || ( 200 * 1024 * 1024 );
23
22
 
23
+ // DOS protection configuration
24
+ const MAX_RPC_MESSAGE_SIZE = toInt( process.env.MAX_RPC_MESSAGE_SIZE ) || ( 1024 * 1024 ); // 1MB default
25
+ const MAX_WS_CONNECTIONS = toInt( process.env.MAX_WS_CONNECTIONS ) || 1000;
26
+ const MAX_CONNECTIONS_PER_IP = toInt( process.env.MAX_CONNECTIONS_PER_IP ) || 50;
27
+ const MAX_CONNECTIONS_TOTAL = toInt( process.env.MAX_CONNECTIONS_TOTAL ) || 1000;
28
+ const REQUEST_TIMEOUT_MS = toInt( process.env.REQUEST_TIMEOUT_MS ) || 30000; // 30 seconds
29
+ const SERVER_TIMEOUT_MS = toInt( process.env.SERVER_TIMEOUT_MS ) || 30000; // 30 seconds
30
+ const KEEP_ALIVE_TIMEOUT_MS = toInt( process.env.KEEP_ALIVE_TIMEOUT_MS ) || 65000; // 65 seconds
31
+ const HEADERS_TIMEOUT_MS = toInt( process.env.HEADERS_TIMEOUT_MS ) || 66000; // 66 seconds
32
+ const RATE_LIMIT_WINDOW_MS = toInt( process.env.RATE_LIMIT_WINDOW_MS ) || ( 15 * 60 * 1000 ); // 15 minutes
33
+ const RATE_LIMIT_MAX_REQUESTS = toInt( process.env.RATE_LIMIT_MAX_REQUESTS ) || 1000; // per window
34
+
24
35
  let dev_mode = false;
25
36
 
37
+ // Connection tracking for DOS protection
38
+ const ipConnections = new Map();
39
+ let totalConnections = 0;
40
+ let activeWSConnections = 0;
41
+
42
+ // Rate limiting storage: Map<ip, {count: number, resetTime: number}>
43
+ const rateLimitStore = new Map();
44
+
45
+ function cleanupRateLimitStore() {
46
+ const now = Date.now();
47
+ for( const [ ip, data ] of rateLimitStore.entries() ) {
48
+ if( now > data.resetTime ) {
49
+ rateLimitStore.delete( ip );
50
+ }
51
+ }
52
+ }
53
+
54
+ // Clean up rate limit store every minute
55
+ setInterval( cleanupRateLimitStore, 60 * 1000 );
56
+
26
57
 
27
58
  function usage() {
28
59
  const base = path.basename( module.filename );
@@ -45,10 +76,112 @@ function next_seq() {
45
76
  }
46
77
 
47
78
 
79
+ function generate_csrf_token() {
80
+ return sha1( "" + ( Date.now() + Math.random() ) );
81
+ }
82
+
83
+
84
+ function parse_cookies( cookie_header ) {
85
+ const cookies = {};
86
+ if( ! cookie_header ) {
87
+ return cookies;
88
+ }
89
+ cookie_header.split( ";" ).forEach( part => {
90
+ const pieces = part.split( "=" );
91
+ const name = pieces.shift().trim();
92
+ if( ! name ) {
93
+ return;
94
+ }
95
+ const value = pieces.join( "=" ).trim();
96
+ const unquoted = value && value[ 0 ] == "\"" && value[ value.length - 1 ] == "\""
97
+ ? value.slice( 1, -1 )
98
+ : value;
99
+ cookies[ name ] = decodeURIComponent( unquoted || "" );
100
+ } );
101
+ return cookies;
102
+ }
103
+
104
+
105
+ function add_set_cookie_header( res, cookie_str ) {
106
+ const existing = res.getHeader( "Set-Cookie" );
107
+ if( ! existing ) {
108
+ res.setHeader( "Set-Cookie", [ cookie_str ] );
109
+ return;
110
+ }
111
+ if( Array.isArray( existing ) ) {
112
+ res.setHeader( "Set-Cookie", existing.concat( cookie_str ) );
113
+ return;
114
+ }
115
+ res.setHeader( "Set-Cookie", [ existing, cookie_str ] );
116
+ }
117
+
118
+
119
+ function build_cookie_string( name, value, options ) {
120
+ const opts = options || {};
121
+ const bits = [];
122
+ bits.push( name + "=" + encodeURIComponent( value || "" ) );
123
+ if( opts.domain ) {
124
+ bits.push( "Domain=" + opts.domain );
125
+ }
126
+ if( opts.path || opts.path === "" ) {
127
+ bits.push( "Path=" + opts.path );
128
+ }
129
+ if( opts.maxAge !== undefined && opts.maxAge !== null ) {
130
+ bits.push( "Max-Age=" + toInt( opts.maxAge ) );
131
+ }
132
+ if( opts.sameSite ) {
133
+ bits.push( "SameSite=" + opts.sameSite );
134
+ }
135
+ if( opts.expires ) {
136
+ bits.push( "Expires=" + opts.expires );
137
+ }
138
+ if( opts.httpOnly ) {
139
+ bits.push( "HttpOnly" );
140
+ }
141
+ if( opts.secure ) {
142
+ bits.push( "Secure" );
143
+ }
144
+ return bits.join( "; " );
145
+ }
146
+
147
+
148
+ function make_cookie_setters( res ) {
149
+ if( ! res ) {
150
+ return {
151
+ setCookie: function( ) {
152
+ W( "setCookie ignored: no response available" );
153
+ },
154
+ clearCookie: function( ) {
155
+ W( "clearCookie ignored: no response available" );
156
+ },
157
+ };
158
+ }
159
+
160
+ const setCookie = function( name, value, options ) {
161
+ const cookie_str = build_cookie_string( name, value, options );
162
+ add_set_cookie_header( res, cookie_str );
163
+ };
164
+
165
+ const clearCookie = function( name, options ) {
166
+ const opts = options ? Object.assign( {}, options ) : {};
167
+ if( ! opts.expires ) {
168
+ opts.expires = "Thu, 01 Jan 1970 00:00:00 GMT";
169
+ }
170
+ opts.maxAge = 0;
171
+ const cookie_str = build_cookie_string( name, "", opts );
172
+ add_set_cookie_header( res, cookie_str );
173
+ };
174
+
175
+ return { setCookie, clearCookie };
176
+ }
177
+
178
+
48
179
  // Handles incoming RPC messages.
49
180
  // Load order for RPC handlers under the provided root:
50
- // 1) Tries explicit CommonJS module at "root/rpc/index.cjs"
51
- // 2) Falls back to legacy directory loader at "root/rpc"
181
+ // 1) Tries explicit CommonJS module at "root/server/index.cjs"
182
+ // 2) Falls back to legacy directory loader at "root/server"
183
+ // 3) Falls back to explicit CommonJS module at "root/rpc/index.cjs"
184
+ // 4) Falls back to legacy directory loader at "root/rpc"
52
185
  // If loading succeeds, passes the message to the handler.
53
186
  // context may be one of:
54
187
  // { transport_type: "WS", connection: { ... } }
@@ -68,14 +201,22 @@ function rpc_handler( root, msg, context, _okay, _fail ) {
68
201
  why = new Error( why );
69
202
  }
70
203
  E( root + ": " + why.stack );
71
- _fail();
204
+ _fail( why );
72
205
  };
73
206
 
207
+ // Check RPC message size
208
+ const msgSize = JSON.stringify( msg ).length;
209
+ if( msgSize > MAX_RPC_MESSAGE_SIZE ) {
210
+ W( "RPC message too large: " + msgSize + " bytes (max: " + MAX_RPC_MESSAGE_SIZE + ")" );
211
+ fail( new Error( "RPC message too large" ) );
212
+ return;
213
+ }
214
+
74
215
  context.upload_dir = UPLOAD_DIR;
75
216
 
76
217
  try {
77
- // try loading explicit common js module
78
- const mod_path = root + "/rpc/index.cjs";
218
+ // try loading explicit common js module from server folder
219
+ const mod_path = root + "/server/index.cjs";
79
220
  const mod = require( mod_path );
80
221
  try {
81
222
  mod( msg, okay, fail, context );
@@ -85,9 +226,9 @@ function rpc_handler( root, msg, context, _okay, _fail ) {
85
226
 
86
227
  } catch( err ) {
87
228
 
88
- // try loading it the old way using the dir
229
+ // try loading it the old way using the server dir
89
230
  try {
90
- const mod_path = root + "/rpc";
231
+ const mod_path = root + "/server";
91
232
  const mod = require( mod_path );
92
233
  try {
93
234
  mod( msg, okay, fail, context );
@@ -96,7 +237,35 @@ function rpc_handler( root, msg, context, _okay, _fail ) {
96
237
  }
97
238
 
98
239
  } catch( err ) {
99
- fail( err );
240
+
241
+ // fall back to rpc folder - try explicit common js module
242
+ try {
243
+ const mod_path = root + "/rpc/index.cjs";
244
+ const mod = require( mod_path );
245
+ try {
246
+ mod( msg, okay, fail, context );
247
+ } catch( err ) {
248
+ fail( err );
249
+ }
250
+
251
+ } catch( err ) {
252
+
253
+ // fall back to rpc folder - try loading it the old way using the dir
254
+ try {
255
+ const mod_path = root + "/rpc";
256
+ const mod = require( mod_path );
257
+ try {
258
+ mod( msg, okay, fail, context );
259
+ } catch( err ) {
260
+ fail( err );
261
+ }
262
+
263
+ } catch( err ) {
264
+ fail( err );
265
+ }
266
+
267
+ }
268
+
100
269
  }
101
270
 
102
271
  }
@@ -106,7 +275,10 @@ function rpc_handler( root, msg, context, _okay, _fail ) {
106
275
  // Glue function to call rpc handler module and then return response
107
276
  // via the websockets msg object
108
277
  function ws_msg_handler( root, msg, host, connection ) {
109
- rpc_handler( root, msg.msg, { transport_type: "WS", host, connection }, data => {
278
+ const { setCookie, clearCookie } = make_cookie_setters();
279
+ const cookies = connection.cookies || {};
280
+
281
+ rpc_handler( root, msg.msg, { transport_type: "WS", host, connection, cookies, setCookie, clearCookie }, data => {
110
282
  msg.reply( data );
111
283
  }, err => {
112
284
  msg.error( err || "Unspecified Error" );
@@ -121,6 +293,75 @@ function populate_query( req, res, next ) {
121
293
  }
122
294
 
123
295
 
296
+ // Rate limiting middleware
297
+ function rate_limit( req, res, next ) {
298
+ const ip = req.socket.remoteAddress || "unknown";
299
+ const now = Date.now();
300
+
301
+ let limitData = rateLimitStore.get( ip );
302
+ if( ! limitData || now > limitData.resetTime ) {
303
+ limitData = { count: 0, resetTime: now + RATE_LIMIT_WINDOW_MS };
304
+ rateLimitStore.set( ip, limitData );
305
+ }
306
+
307
+ limitData.count++;
308
+
309
+ if( limitData.count > RATE_LIMIT_MAX_REQUESTS ) {
310
+ W( "Rate limit exceeded for IP: " + ip + " (" + limitData.count + " requests)" );
311
+ res.writeHead( 429, {
312
+ "Content-Type": "application/json",
313
+ "Retry-After": Math.ceil( ( limitData.resetTime - now ) / 1000 ),
314
+ } );
315
+ res.write( o2j( { error: "Too many requests" } ) );
316
+ res.end();
317
+ return;
318
+ }
319
+
320
+ next();
321
+ }
322
+
323
+
324
+ // Request timeout middleware
325
+ function timeout_middleware( req, res, next ) {
326
+ const timeout = setTimeout( function( ) {
327
+ if( ! res.headersSent ) {
328
+ W( "Request timeout for " + req.method + " " + req.url );
329
+ res.writeHead( 408, { "Content-Type": "application/json" } );
330
+ res.write( o2j( { error: "Request timeout" } ) );
331
+ res.end();
332
+ }
333
+ }, REQUEST_TIMEOUT_MS );
334
+
335
+ res.on( "finish", function( ) {
336
+ clearTimeout( timeout );
337
+ } );
338
+
339
+ next();
340
+ }
341
+
342
+
343
+ // Per-IP connection limit check
344
+ function check_ip_connection_limit( ip ) {
345
+ const count = ipConnections.get( ip ) || 0;
346
+ if( count >= MAX_CONNECTIONS_PER_IP ) {
347
+ return false;
348
+ }
349
+ ipConnections.set( ip, count + 1 );
350
+ return true;
351
+ }
352
+
353
+
354
+ function release_ip_connection( ip ) {
355
+ const count = ipConnections.get( ip ) || 0;
356
+ if( count > 0 ) {
357
+ ipConnections.set( ip, count - 1 );
358
+ if( count === 1 ) {
359
+ ipConnections.delete( ip );
360
+ }
361
+ }
362
+ }
363
+
364
+
124
365
  // Simple logging handler
125
366
  function logger( req, res, next ) {
126
367
  const host = req.headers[ "host" ];
@@ -131,6 +372,92 @@ function logger( req, res, next ) {
131
372
  }
132
373
 
133
374
 
375
+ // CSRF protection middleware using double-submit cookie pattern
376
+ function csrf_protection( root ) {
377
+ return function( req, res, next ) {
378
+ const cookies = parse_cookies( req.headers[ "cookie" ] );
379
+ const { setCookie } = make_cookie_setters( res );
380
+ const host = req.headers[ "host" ];
381
+ const method = req.method;
382
+ const url = req.url.split( "?" ).shift();
383
+
384
+ // Generate CSRF token if it doesn't exist
385
+ if( ! cookies.csrf_token ) {
386
+ const token = generate_csrf_token();
387
+ const cookieOptions = {
388
+ sameSite: "Strict",
389
+ path: "/",
390
+ maxAge: 86400 * 365, // 1 year
391
+ };
392
+ // Set Secure flag in TLS mode (when not in dev mode)
393
+ if( ! dev_mode ) {
394
+ cookieOptions.secure = true;
395
+ }
396
+ setCookie( "csrf_token", token, cookieOptions );
397
+ cookies.csrf_token = token;
398
+ }
399
+
400
+ // Skip validation for GET requests, WebSocket upgrades, and static files
401
+ if( method === "GET" ) {
402
+ next();
403
+ return;
404
+ }
405
+
406
+ // Only validate POST and PUT requests to /rpc or any PUT request
407
+ const is_rpc = ( url === "/rpc" || url === "/rpc/" );
408
+ const is_put = ( method === "PUT" );
409
+
410
+ if( ! is_rpc && ! is_put ) {
411
+ next();
412
+ return;
413
+ }
414
+
415
+ // Validate CSRF token (double-submit cookie pattern)
416
+ const cookieToken = cookies.csrf_token;
417
+ const headerToken = req.headers[ "x-csrf-token" ];
418
+
419
+ if( ! cookieToken || ! headerToken || cookieToken !== headerToken ) {
420
+ W( "CSRF token validation failed for " + method + " " + url );
421
+ res.writeHead( 403, {
422
+ "Content-Type": "application/json",
423
+ "Cache-Control": "no-store",
424
+ } );
425
+ res.write( o2j( { error: "CSRF token validation failed" } ) );
426
+ res.end();
427
+ return;
428
+ }
429
+
430
+ // Additional Origin/Referer validation
431
+ const origin = req.headers[ "origin" ];
432
+ const referer = req.headers[ "referer" ];
433
+ let originValid = false;
434
+
435
+ if( origin ) {
436
+ // Check if origin matches host (with protocol)
437
+ const expectedOrigin = ( dev_mode ? "http://" : "https://" ) + host;
438
+ originValid = origin === expectedOrigin;
439
+ } else if( referer ) {
440
+ // Fall back to Referer if Origin is missing
441
+ const expectedReferer = ( dev_mode ? "http://" : "https://" ) + host;
442
+ originValid = referer.startsWith( expectedReferer );
443
+ }
444
+
445
+ if( ! originValid ) {
446
+ W( "CSRF Origin/Referer validation failed for " + method + " " + url + " origin: " + origin + " referer: " + referer );
447
+ res.writeHead( 403, {
448
+ "Content-Type": "application/json",
449
+ "Cache-Control": "no-store",
450
+ } );
451
+ res.write( o2j( { error: "CSRF token validation failed" } ) );
452
+ res.end();
453
+ return;
454
+ }
455
+
456
+ next();
457
+ };
458
+ }
459
+
460
+
134
461
  // Creates and returns a handler that intercepts and services requests to
135
462
  // the "/rpc" endpoint. In TLS mode only POST is allowed; in dev mode
136
463
  // GET is also allowed with query-string payloads.
@@ -160,7 +487,16 @@ function rpc_post( root ) {
160
487
  D( method + " >------> " + o2j( input, null, 2 ) );
161
488
 
162
489
  // Summon the rpc handler for the domain root.
163
- rpc_handler( root, input, { transport_type: method, connection: { host: req.headers[ "host" ], req, res, } } , output => {
490
+ const cookies = parse_cookies( req.headers[ "cookie" ] );
491
+ const { setCookie, clearCookie } = make_cookie_setters( res );
492
+
493
+ rpc_handler( root, input, {
494
+ transport_type: method,
495
+ connection: { host: req.headers[ "host" ], req, res, },
496
+ cookies,
497
+ setCookie,
498
+ clearCookie,
499
+ } , output => {
164
500
  res.writeHead( 200, {
165
501
  "Content-Type": "application/json",
166
502
  "Cache-Control": "no-store",
@@ -168,11 +504,13 @@ function rpc_post( root ) {
168
504
  D( method + " <------< " + o2j( output, null, 2 ) );
169
505
  res.write( o2j( output ) );
170
506
  res.end();
171
- }, () => {
507
+ }, ( err ) => {
508
+ const error_message = err && err.message ? err.message : ( err || "Internal server error" );
172
509
  res.writeHead( 500, {
173
510
  "Content-Type": "application/json",
174
511
  "Cache-Control": "no-store",
175
512
  } );
513
+ res.write( o2j( { error: error_message } ) );
176
514
  res.end();
177
515
  } );
178
516
  };
@@ -290,11 +628,13 @@ function put_handler( req, res, next ) {
290
628
  // default (basic) functionality.
291
629
  function basic_handler( root ) {
292
630
  const app = connect();
631
+ app.use( timeout_middleware );
632
+ app.use( rate_limit );
293
633
  app.use( body_parser );
294
634
  app.use( compression );
295
- app.use( cors ); // allow requests from other domains
296
635
  app.use( populate_query );
297
636
  app.use( logger );
637
+ app.use( csrf_protection( root ) );
298
638
  app.use( put_handler )
299
639
  app.use( rpc_post( root ) );
300
640
  app.use( rpc_static );
@@ -310,7 +650,39 @@ const cached_basic_handlers = {};
310
650
  // Handle REST calls (as opposed to websocket messages)
311
651
  function rest_handler( root, req, rsp ) {
312
652
 
313
- const remote_ip = req.socket.remoteAddress;
653
+ const remote_ip = req.socket.remoteAddress || "unknown";
654
+
655
+ // Check total connection limit
656
+ if( totalConnections >= MAX_CONNECTIONS_TOTAL ) {
657
+ W( "Max connections exceeded: " + totalConnections );
658
+ rsp.writeHead( 503, { "Content-Type": "application/json" } );
659
+ rsp.write( o2j( { error: "Service temporarily unavailable" } ) );
660
+ rsp.end();
661
+ return;
662
+ }
663
+
664
+ // Check per-IP connection limit
665
+ if( ! check_ip_connection_limit( remote_ip ) ) {
666
+ W( "Max connections per IP exceeded for: " + remote_ip );
667
+ rsp.writeHead( 503, { "Content-Type": "application/json" } );
668
+ rsp.write( o2j( { error: "Too many connections from this IP" } ) );
669
+ rsp.end();
670
+ return;
671
+ }
672
+
673
+ totalConnections++;
674
+
675
+ // Release connection tracking on response finish
676
+ rsp.on( "finish", function( ) {
677
+ totalConnections--;
678
+ release_ip_connection( remote_ip );
679
+ } );
680
+
681
+ rsp.on( "close", function( ) {
682
+ totalConnections--;
683
+ release_ip_connection( remote_ip );
684
+ } );
685
+
314
686
  I( remote_ip + " " + req.headers[ "host" ] + ": " + req.method + " " + req.url );
315
687
  D( "rest_handler root: " + root );
316
688
 
@@ -357,9 +729,50 @@ function ws_attach( server, msg_handler ) {
357
729
 
358
730
  V( "WS: connection request from "+wsreq.remoteAddress+" "+wsreq.resource )
359
731
 
732
+ const remote_ip = wsreq.remoteAddress || "unknown";
733
+
734
+ // Check WebSocket connection limit
735
+ if( activeWSConnections >= MAX_WS_CONNECTIONS ) {
736
+ W( "Max WebSocket connections exceeded: " + activeWSConnections );
737
+ wsreq.reject( 503, "Too many WebSocket connections" );
738
+ return;
739
+ }
740
+
741
+ // Check per-IP connection limit
742
+ if( ! check_ip_connection_limit( remote_ip ) ) {
743
+ W( "Max connections per IP exceeded for WebSocket: " + remote_ip );
744
+ wsreq.reject( 503, "Too many connections from this IP" );
745
+ return;
746
+ }
747
+
748
+ // Check total connection limit
749
+ if( totalConnections >= MAX_CONNECTIONS_TOTAL ) {
750
+ W( "Max total connections exceeded: " + totalConnections );
751
+ wsreq.reject( 503, "Service temporarily unavailable" );
752
+ return;
753
+ }
754
+
755
+ activeWSConnections++;
756
+ totalConnections++;
757
+
360
758
  const host = wsreq.httpRequest.headers[ "host" ];
759
+ const cookies = parse_cookies( wsreq.httpRequest.headers[ "cookie" ] );
760
+ const origin = wsreq.origin;
761
+
762
+ // Validate WebSocket origin matches host
763
+ if( origin ) {
764
+ const expectedOrigin = ( dev_mode ? "http://" : "https://" ) + host;
765
+ if( origin !== expectedOrigin ) {
766
+ W( "WS: Origin validation failed: " + origin + " expected: " + expectedOrigin );
767
+ activeWSConnections--;
768
+ totalConnections--;
769
+ release_ip_connection( remote_ip );
770
+ wsreq.reject( 403, "Origin validation failed" );
771
+ return;
772
+ }
773
+ }
361
774
 
362
- const socket = wsreq.accept( null, wsreq.origin || "*" );
775
+ const socket = wsreq.accept( null, origin || host );
363
776
 
364
777
  const name = "ws-conn-" + next_seq(); // XXX just use the websocket id
365
778
 
@@ -373,20 +786,33 @@ function ws_attach( server, msg_handler ) {
373
786
  socket.send( o2j( msg ) );
374
787
  };
375
788
 
376
- const conn = { name, socket, send };
789
+ const conn = { name, socket, send, cookies };
377
790
 
378
791
  socket.on( "error", function( err ) {
379
792
  E( "WS: error", err.stack || err );
793
+ activeWSConnections--;
794
+ totalConnections--;
795
+ release_ip_connection( remote_ip );
380
796
  });
381
797
 
382
798
  socket.on("close", function() {
383
799
  D( "WS: disconnect" );
800
+ activeWSConnections--;
801
+ totalConnections--;
802
+ release_ip_connection( remote_ip );
384
803
  });
385
804
 
386
805
  // incoming msgs from client come through here
387
806
  socket.on( "message", function( x ) {
388
807
  D( "WS >------> "+x.utf8Data );
389
808
 
809
+ // Check WebSocket message size
810
+ if( x.utf8Data && x.utf8Data.length > MAX_RPC_MESSAGE_SIZE ) {
811
+ W( "WS message too large: " + x.utf8Data.length + " bytes (max: " + MAX_RPC_MESSAGE_SIZE + ")" );
812
+ socket.close( 1009, "Message too large" );
813
+ return;
814
+ }
815
+
390
816
  const json = x.utf8Data; // raw message is a utf8 string
391
817
  const msg_in = j2o( json );
392
818
  if( msg_in == null ) {
@@ -433,7 +859,9 @@ if( argv.length == 2 ) {
433
859
 
434
860
  L( toInt( VERBOSITY ) );
435
861
 
862
+ const pkg = require( __dirname + "/package.json" );
436
863
  I( "=== TLS MODE ===" );
864
+ I( "Version: " + pkg.version );
437
865
  V( "DOMAINS_ROOT: " + DOMAINS_ROOT );
438
866
  V( "MAINTAINER_EMAIL: " + MAINTAINER_EMAIL );
439
867
  V( "VERBOSITY: " + VERBOSITY );
@@ -446,6 +874,12 @@ if( argv.length == 2 ) {
446
874
  } ).ready( glx => {
447
875
 
448
876
  var server = glx.httpsServer();
877
+
878
+ // Configure server timeouts and connection limits
879
+ server.maxConnections = MAX_CONNECTIONS_TOTAL;
880
+ server.timeout = SERVER_TIMEOUT_MS;
881
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
882
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
449
883
 
450
884
  ws_attach( server, function( msg, connection, host ) {
451
885
  const root = path.resolve( DOMAINS_ROOT + "/" + host );
@@ -480,7 +914,9 @@ if( argv.length == 5 ) {
480
914
 
481
915
  SITE_ROOT = path.resolve( SITE_ROOT );
482
916
 
917
+ const pkg = require( __dirname + "/package.json" );
483
918
  I( "=== DEV MODE ===" );
919
+ I( "Version: " + pkg.version );
484
920
  V( "VERBOSITY: " + VERBOSITY );
485
921
  V( "SITE_ROOT: " + SITE_ROOT );
486
922
  V( "PORT: " + PORT );
@@ -488,6 +924,12 @@ if( argv.length == 5 ) {
488
924
  const server = http.createServer( ( req, res ) => {
489
925
  rest_handler( SITE_ROOT, req, res );
490
926
  } );
927
+
928
+ // Configure server timeouts and connection limits
929
+ server.maxConnections = MAX_CONNECTIONS_TOTAL;
930
+ server.timeout = SERVER_TIMEOUT_MS;
931
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
932
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
491
933
 
492
934
  ws_attach( server, ( msg, connection, host ) => {
493
935
  ws_msg_handler( SITE_ROOT, msg, host, connection );
@@ -508,7 +950,8 @@ if( argv.length == 3 && argv[ 2 ] == "-v" ) {
508
950
 
509
951
  // pull package version from package.json and print it
510
952
  const p = require( __dirname + "/package.json" );
511
- I( p.version );
953
+ console.log( p.version );
954
+ process.exit();
512
955
 
513
956
  }
514
957
  else {