tlsd 2.16.3 → 2.18.1

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.1",
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,18 @@ 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, {
282
+ transport_type: "WS",
283
+ host,
284
+ connection,
285
+ cookies,
286
+ setCookie,
287
+ clearCookie,
288
+ dev_mode,
289
+ }, data => {
110
290
  msg.reply( data );
111
291
  }, err => {
112
292
  msg.error( err || "Unspecified Error" );
@@ -121,6 +301,75 @@ function populate_query( req, res, next ) {
121
301
  }
122
302
 
123
303
 
304
+ // Rate limiting middleware
305
+ function rate_limit( req, res, next ) {
306
+ const ip = req.socket.remoteAddress || "unknown";
307
+ const now = Date.now();
308
+
309
+ let limitData = rateLimitStore.get( ip );
310
+ if( ! limitData || now > limitData.resetTime ) {
311
+ limitData = { count: 0, resetTime: now + RATE_LIMIT_WINDOW_MS };
312
+ rateLimitStore.set( ip, limitData );
313
+ }
314
+
315
+ limitData.count++;
316
+
317
+ if( limitData.count > RATE_LIMIT_MAX_REQUESTS ) {
318
+ W( "Rate limit exceeded for IP: " + ip + " (" + limitData.count + " requests)" );
319
+ res.writeHead( 429, {
320
+ "Content-Type": "application/json",
321
+ "Retry-After": Math.ceil( ( limitData.resetTime - now ) / 1000 ),
322
+ } );
323
+ res.write( o2j( { error: "Too many requests" } ) );
324
+ res.end();
325
+ return;
326
+ }
327
+
328
+ next();
329
+ }
330
+
331
+
332
+ // Request timeout middleware
333
+ function timeout_middleware( req, res, next ) {
334
+ const timeout = setTimeout( function( ) {
335
+ if( ! res.headersSent ) {
336
+ W( "Request timeout for " + req.method + " " + req.url );
337
+ res.writeHead( 408, { "Content-Type": "application/json" } );
338
+ res.write( o2j( { error: "Request timeout" } ) );
339
+ res.end();
340
+ }
341
+ }, REQUEST_TIMEOUT_MS );
342
+
343
+ res.on( "finish", function( ) {
344
+ clearTimeout( timeout );
345
+ } );
346
+
347
+ next();
348
+ }
349
+
350
+
351
+ // Per-IP connection limit check
352
+ function check_ip_connection_limit( ip ) {
353
+ const count = ipConnections.get( ip ) || 0;
354
+ if( count >= MAX_CONNECTIONS_PER_IP ) {
355
+ return false;
356
+ }
357
+ ipConnections.set( ip, count + 1 );
358
+ return true;
359
+ }
360
+
361
+
362
+ function release_ip_connection( ip ) {
363
+ const count = ipConnections.get( ip ) || 0;
364
+ if( count > 0 ) {
365
+ ipConnections.set( ip, count - 1 );
366
+ if( count === 1 ) {
367
+ ipConnections.delete( ip );
368
+ }
369
+ }
370
+ }
371
+
372
+
124
373
  // Simple logging handler
125
374
  function logger( req, res, next ) {
126
375
  const host = req.headers[ "host" ];
@@ -131,6 +380,92 @@ function logger( req, res, next ) {
131
380
  }
132
381
 
133
382
 
383
+ // CSRF protection middleware using double-submit cookie pattern
384
+ function csrf_protection( root ) {
385
+ return function( req, res, next ) {
386
+ const cookies = parse_cookies( req.headers[ "cookie" ] );
387
+ const { setCookie } = make_cookie_setters( res );
388
+ const host = req.headers[ "host" ];
389
+ const method = req.method;
390
+ const url = req.url.split( "?" ).shift();
391
+
392
+ // Generate CSRF token if it doesn't exist
393
+ if( ! cookies.csrf_token ) {
394
+ const token = generate_csrf_token();
395
+ const cookieOptions = {
396
+ sameSite: "Strict",
397
+ path: "/",
398
+ maxAge: 86400 * 365, // 1 year
399
+ };
400
+ // Set Secure flag in TLS mode (when not in dev mode)
401
+ if( ! dev_mode ) {
402
+ cookieOptions.secure = true;
403
+ }
404
+ setCookie( "csrf_token", token, cookieOptions );
405
+ cookies.csrf_token = token;
406
+ }
407
+
408
+ // Skip validation for GET requests, WebSocket upgrades, and static files
409
+ if( method === "GET" ) {
410
+ next();
411
+ return;
412
+ }
413
+
414
+ // Only validate POST and PUT requests to /rpc or any PUT request
415
+ const is_rpc = ( url === "/rpc" || url === "/rpc/" );
416
+ const is_put = ( method === "PUT" );
417
+
418
+ if( ! is_rpc && ! is_put ) {
419
+ next();
420
+ return;
421
+ }
422
+
423
+ // Validate CSRF token (double-submit cookie pattern)
424
+ const cookieToken = cookies.csrf_token;
425
+ const headerToken = req.headers[ "x-csrf-token" ];
426
+
427
+ if( ! cookieToken || ! headerToken || cookieToken !== headerToken ) {
428
+ W( "CSRF token validation failed for " + method + " " + url );
429
+ res.writeHead( 403, {
430
+ "Content-Type": "application/json",
431
+ "Cache-Control": "no-store",
432
+ } );
433
+ res.write( o2j( { error: "CSRF token validation failed" } ) );
434
+ res.end();
435
+ return;
436
+ }
437
+
438
+ // Additional Origin/Referer validation
439
+ const origin = req.headers[ "origin" ];
440
+ const referer = req.headers[ "referer" ];
441
+ let originValid = false;
442
+
443
+ if( origin ) {
444
+ // Check if origin matches host (with protocol)
445
+ const expectedOrigin = ( dev_mode ? "http://" : "https://" ) + host;
446
+ originValid = origin === expectedOrigin;
447
+ } else if( referer ) {
448
+ // Fall back to Referer if Origin is missing
449
+ const expectedReferer = ( dev_mode ? "http://" : "https://" ) + host;
450
+ originValid = referer.startsWith( expectedReferer );
451
+ }
452
+
453
+ if( ! originValid ) {
454
+ W( "CSRF Origin/Referer validation failed for " + method + " " + url + " origin: " + origin + " referer: " + referer );
455
+ res.writeHead( 403, {
456
+ "Content-Type": "application/json",
457
+ "Cache-Control": "no-store",
458
+ } );
459
+ res.write( o2j( { error: "CSRF token validation failed" } ) );
460
+ res.end();
461
+ return;
462
+ }
463
+
464
+ next();
465
+ };
466
+ }
467
+
468
+
134
469
  // Creates and returns a handler that intercepts and services requests to
135
470
  // the "/rpc" endpoint. In TLS mode only POST is allowed; in dev mode
136
471
  // GET is also allowed with query-string payloads.
@@ -160,7 +495,17 @@ function rpc_post( root ) {
160
495
  D( method + " >------> " + o2j( input, null, 2 ) );
161
496
 
162
497
  // Summon the rpc handler for the domain root.
163
- rpc_handler( root, input, { transport_type: method, connection: { host: req.headers[ "host" ], req, res, } } , output => {
498
+ const cookies = parse_cookies( req.headers[ "cookie" ] );
499
+ const { setCookie, clearCookie } = make_cookie_setters( res );
500
+
501
+ rpc_handler( root, input, {
502
+ transport_type: method,
503
+ connection: { host: req.headers[ "host" ], req, res, },
504
+ cookies,
505
+ setCookie,
506
+ clearCookie,
507
+ dev_mode,
508
+ } , output => {
164
509
  res.writeHead( 200, {
165
510
  "Content-Type": "application/json",
166
511
  "Cache-Control": "no-store",
@@ -168,11 +513,13 @@ function rpc_post( root ) {
168
513
  D( method + " <------< " + o2j( output, null, 2 ) );
169
514
  res.write( o2j( output ) );
170
515
  res.end();
171
- }, () => {
516
+ }, ( err ) => {
517
+ const error_message = err && err.message ? err.message : ( err || "Internal server error" );
172
518
  res.writeHead( 500, {
173
519
  "Content-Type": "application/json",
174
520
  "Cache-Control": "no-store",
175
521
  } );
522
+ res.write( o2j( { error: error_message } ) );
176
523
  res.end();
177
524
  } );
178
525
  };
@@ -290,11 +637,13 @@ function put_handler( req, res, next ) {
290
637
  // default (basic) functionality.
291
638
  function basic_handler( root ) {
292
639
  const app = connect();
640
+ app.use( timeout_middleware );
641
+ app.use( rate_limit );
293
642
  app.use( body_parser );
294
643
  app.use( compression );
295
- app.use( cors ); // allow requests from other domains
296
644
  app.use( populate_query );
297
645
  app.use( logger );
646
+ app.use( csrf_protection( root ) );
298
647
  app.use( put_handler )
299
648
  app.use( rpc_post( root ) );
300
649
  app.use( rpc_static );
@@ -310,7 +659,39 @@ const cached_basic_handlers = {};
310
659
  // Handle REST calls (as opposed to websocket messages)
311
660
  function rest_handler( root, req, rsp ) {
312
661
 
313
- const remote_ip = req.socket.remoteAddress;
662
+ const remote_ip = req.socket.remoteAddress || "unknown";
663
+
664
+ // Check total connection limit
665
+ if( totalConnections >= MAX_CONNECTIONS_TOTAL ) {
666
+ W( "Max connections exceeded: " + totalConnections );
667
+ rsp.writeHead( 503, { "Content-Type": "application/json" } );
668
+ rsp.write( o2j( { error: "Service temporarily unavailable" } ) );
669
+ rsp.end();
670
+ return;
671
+ }
672
+
673
+ // Check per-IP connection limit
674
+ if( ! check_ip_connection_limit( remote_ip ) ) {
675
+ W( "Max connections per IP exceeded for: " + remote_ip );
676
+ rsp.writeHead( 503, { "Content-Type": "application/json" } );
677
+ rsp.write( o2j( { error: "Too many connections from this IP" } ) );
678
+ rsp.end();
679
+ return;
680
+ }
681
+
682
+ totalConnections++;
683
+
684
+ // Release connection tracking on response finish
685
+ rsp.on( "finish", function( ) {
686
+ totalConnections--;
687
+ release_ip_connection( remote_ip );
688
+ } );
689
+
690
+ rsp.on( "close", function( ) {
691
+ totalConnections--;
692
+ release_ip_connection( remote_ip );
693
+ } );
694
+
314
695
  I( remote_ip + " " + req.headers[ "host" ] + ": " + req.method + " " + req.url );
315
696
  D( "rest_handler root: " + root );
316
697
 
@@ -357,9 +738,50 @@ function ws_attach( server, msg_handler ) {
357
738
 
358
739
  V( "WS: connection request from "+wsreq.remoteAddress+" "+wsreq.resource )
359
740
 
741
+ const remote_ip = wsreq.remoteAddress || "unknown";
742
+
743
+ // Check WebSocket connection limit
744
+ if( activeWSConnections >= MAX_WS_CONNECTIONS ) {
745
+ W( "Max WebSocket connections exceeded: " + activeWSConnections );
746
+ wsreq.reject( 503, "Too many WebSocket connections" );
747
+ return;
748
+ }
749
+
750
+ // Check per-IP connection limit
751
+ if( ! check_ip_connection_limit( remote_ip ) ) {
752
+ W( "Max connections per IP exceeded for WebSocket: " + remote_ip );
753
+ wsreq.reject( 503, "Too many connections from this IP" );
754
+ return;
755
+ }
756
+
757
+ // Check total connection limit
758
+ if( totalConnections >= MAX_CONNECTIONS_TOTAL ) {
759
+ W( "Max total connections exceeded: " + totalConnections );
760
+ wsreq.reject( 503, "Service temporarily unavailable" );
761
+ return;
762
+ }
763
+
764
+ activeWSConnections++;
765
+ totalConnections++;
766
+
360
767
  const host = wsreq.httpRequest.headers[ "host" ];
768
+ const cookies = parse_cookies( wsreq.httpRequest.headers[ "cookie" ] );
769
+ const origin = wsreq.origin;
770
+
771
+ // Validate WebSocket origin matches host
772
+ if( origin ) {
773
+ const expectedOrigin = ( dev_mode ? "http://" : "https://" ) + host;
774
+ if( origin !== expectedOrigin ) {
775
+ W( "WS: Origin validation failed: " + origin + " expected: " + expectedOrigin );
776
+ activeWSConnections--;
777
+ totalConnections--;
778
+ release_ip_connection( remote_ip );
779
+ wsreq.reject( 403, "Origin validation failed" );
780
+ return;
781
+ }
782
+ }
361
783
 
362
- const socket = wsreq.accept( null, wsreq.origin || "*" );
784
+ const socket = wsreq.accept( null, origin || host );
363
785
 
364
786
  const name = "ws-conn-" + next_seq(); // XXX just use the websocket id
365
787
 
@@ -373,20 +795,33 @@ function ws_attach( server, msg_handler ) {
373
795
  socket.send( o2j( msg ) );
374
796
  };
375
797
 
376
- const conn = { name, socket, send };
798
+ const conn = { name, socket, send, cookies };
377
799
 
378
800
  socket.on( "error", function( err ) {
379
801
  E( "WS: error", err.stack || err );
802
+ activeWSConnections--;
803
+ totalConnections--;
804
+ release_ip_connection( remote_ip );
380
805
  });
381
806
 
382
807
  socket.on("close", function() {
383
808
  D( "WS: disconnect" );
809
+ activeWSConnections--;
810
+ totalConnections--;
811
+ release_ip_connection( remote_ip );
384
812
  });
385
813
 
386
814
  // incoming msgs from client come through here
387
815
  socket.on( "message", function( x ) {
388
816
  D( "WS >------> "+x.utf8Data );
389
817
 
818
+ // Check WebSocket message size
819
+ if( x.utf8Data && x.utf8Data.length > MAX_RPC_MESSAGE_SIZE ) {
820
+ W( "WS message too large: " + x.utf8Data.length + " bytes (max: " + MAX_RPC_MESSAGE_SIZE + ")" );
821
+ socket.close( 1009, "Message too large" );
822
+ return;
823
+ }
824
+
390
825
  const json = x.utf8Data; // raw message is a utf8 string
391
826
  const msg_in = j2o( json );
392
827
  if( msg_in == null ) {
@@ -433,7 +868,9 @@ if( argv.length == 2 ) {
433
868
 
434
869
  L( toInt( VERBOSITY ) );
435
870
 
871
+ const pkg = require( __dirname + "/package.json" );
436
872
  I( "=== TLS MODE ===" );
873
+ I( "Version: " + pkg.version );
437
874
  V( "DOMAINS_ROOT: " + DOMAINS_ROOT );
438
875
  V( "MAINTAINER_EMAIL: " + MAINTAINER_EMAIL );
439
876
  V( "VERBOSITY: " + VERBOSITY );
@@ -446,6 +883,12 @@ if( argv.length == 2 ) {
446
883
  } ).ready( glx => {
447
884
 
448
885
  var server = glx.httpsServer();
886
+
887
+ // Configure server timeouts and connection limits
888
+ server.maxConnections = MAX_CONNECTIONS_TOTAL;
889
+ server.timeout = SERVER_TIMEOUT_MS;
890
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
891
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
449
892
 
450
893
  ws_attach( server, function( msg, connection, host ) {
451
894
  const root = path.resolve( DOMAINS_ROOT + "/" + host );
@@ -480,7 +923,9 @@ if( argv.length == 5 ) {
480
923
 
481
924
  SITE_ROOT = path.resolve( SITE_ROOT );
482
925
 
926
+ const pkg = require( __dirname + "/package.json" );
483
927
  I( "=== DEV MODE ===" );
928
+ I( "Version: " + pkg.version );
484
929
  V( "VERBOSITY: " + VERBOSITY );
485
930
  V( "SITE_ROOT: " + SITE_ROOT );
486
931
  V( "PORT: " + PORT );
@@ -488,6 +933,12 @@ if( argv.length == 5 ) {
488
933
  const server = http.createServer( ( req, res ) => {
489
934
  rest_handler( SITE_ROOT, req, res );
490
935
  } );
936
+
937
+ // Configure server timeouts and connection limits
938
+ server.maxConnections = MAX_CONNECTIONS_TOTAL;
939
+ server.timeout = SERVER_TIMEOUT_MS;
940
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
941
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
491
942
 
492
943
  ws_attach( server, ( msg, connection, host ) => {
493
944
  ws_msg_handler( SITE_ROOT, msg, host, connection );
@@ -508,7 +959,8 @@ if( argv.length == 3 && argv[ 2 ] == "-v" ) {
508
959
 
509
960
  // pull package version from package.json and print it
510
961
  const p = require( __dirname + "/package.json" );
511
- I( p.version );
962
+ console.log( p.version );
963
+ process.exit();
512
964
 
513
965
  }
514
966
  else {