tlsd 2.18.1 → 2.20.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.18.1",
3
+ "version": "2.20.0",
4
4
  "description": "A server for web app prototyping with HTTPS and Websockets",
5
5
  "main": "tlsd.js",
6
6
  "bin": {
@@ -45,6 +45,7 @@
45
45
  const uploadURL = '/';
46
46
  xhr.open( 'PUT', uploadURL, true );
47
47
  xhr.setRequestHeader( 'Content-Type', file.type || 'application/octet-stream' );
48
+ xhr.setRequestHeader( 'X-Filename', file.name );
48
49
  xhr.onload = () => {
49
50
  if( xhr.status === 200 ) {
50
51
  let result = JSON.parse( xhr.responseText );
package/tlsd.js CHANGED
@@ -20,6 +20,15 @@ const { D, V, I, W, E } = L;
20
20
  const UPLOAD_DIR = process.env.UPLOAD_DIR || "/tmp/tlsd-put-files/";
21
21
  const UPLOAD_MAX_BYTES = toInt( process.env.UPLOAD_MAX_BYTES ) || ( 200 * 1024 * 1024 );
22
22
 
23
+ function sanitize_upload_filename( raw ) {
24
+ if( ! raw || typeof raw !== "string" ) return "file";
25
+ const basename = path.basename( raw.trim() );
26
+ const sanitized = basename.replace( /[\\/:*?"<>|\x00-\x1f]/g, "_" ).replace( /\.{2,}/g, "." );
27
+ // Limit to the LAST 40 chars of the sanitized filename
28
+ const truncated = sanitized.slice( -40 );
29
+ return truncated || "file";
30
+ }
31
+
23
32
  // DOS protection configuration
24
33
  const MAX_RPC_MESSAGE_SIZE = toInt( process.env.MAX_RPC_MESSAGE_SIZE ) || ( 1024 * 1024 ); // 1MB default
25
34
  const MAX_WS_CONNECTIONS = toInt( process.env.MAX_WS_CONNECTIONS ) || 1000;
@@ -184,7 +193,7 @@ function make_cookie_setters( res ) {
184
193
  // 4) Falls back to legacy directory loader at "root/rpc"
185
194
  // If loading succeeds, passes the message to the handler.
186
195
  // context may be one of:
187
- // { transport_type: "WS", connection: { ... } }
196
+ // { transport_type: "WS", connection: { name, socket, send, cookies, resource }, ... }
188
197
  // { transport_type: "GET", connection: { req, res } }
189
198
  // { transport_type: "POST", connection: { req, res } }
190
199
  // Additionally, context.upload_dir is set to the server's upload directory.
@@ -420,36 +429,47 @@ function csrf_protection( root ) {
420
429
  return;
421
430
  }
422
431
 
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();
432
+ // In dev mode, skip CSRF validation for PUT (e.g. drag-drop file uploads)
433
+ if( dev_mode && is_put ) {
434
+ next();
435
435
  return;
436
436
  }
437
437
 
438
- // Additional Origin/Referer validation
438
+ const cookieToken = cookies.csrf_token;
439
+ const headerToken = req.headers[ "x-csrf-token" ];
440
+
441
+ // Origin/Referer validation
439
442
  const origin = req.headers[ "origin" ];
440
443
  const referer = req.headers[ "referer" ];
441
444
  let originValid = false;
442
445
 
443
446
  if( origin ) {
444
- // Check if origin matches host (with protocol)
445
447
  const expectedOrigin = ( dev_mode ? "http://" : "https://" ) + host;
446
448
  originValid = origin === expectedOrigin;
447
449
  } else if( referer ) {
448
- // Fall back to Referer if Origin is missing
449
450
  const expectedReferer = ( dev_mode ? "http://" : "https://" ) + host;
450
451
  originValid = referer.startsWith( expectedReferer );
451
452
  }
452
453
 
454
+ // For PUT (e.g. file uploads), cookie + Origin/Referer is sufficient.
455
+ // Same-origin XHR sends these automatically; no client-side token handling needed.
456
+ if( is_put && ! is_rpc && cookieToken && originValid ) {
457
+ next();
458
+ return;
459
+ }
460
+
461
+ // Validate CSRF token (double-submit cookie pattern)
462
+ if( ! cookieToken || ! headerToken || cookieToken !== headerToken ) {
463
+ W( "CSRF token validation failed for " + method + " " + url );
464
+ res.writeHead( 403, {
465
+ "Content-Type": "application/json",
466
+ "Cache-Control": "no-store",
467
+ } );
468
+ res.write( o2j( { error: "CSRF token validation failed" } ) );
469
+ res.end();
470
+ return;
471
+ }
472
+
453
473
  if( ! originValid ) {
454
474
  W( "CSRF Origin/Referer validation failed for " + method + " " + url + " origin: " + origin + " referer: " + referer );
455
475
  res.writeHead( 403, {
@@ -582,8 +602,17 @@ function put_handler( req, res, next ) {
582
602
  fs.mkdirSync( local_path, { recursive: true } );
583
603
 
584
604
  // generate random hash to store file under locally
585
- const hash = sha1( "" + ( Date.now() + Math.random() ) );
586
- local_path += "/" + hash;
605
+ const hash = sha1( "" + ( Date.now() + Math.random() ) ).slice( 0, 16 ); // only use first 16 chars of the hash
606
+ let raw_filename = req.headers[ "X-Filename" ];
607
+ if( ! raw_filename ) {
608
+ // take the filename from the last part of the URL
609
+ raw_filename = req.url.split( "/" ).pop();
610
+ // remove any query string
611
+ raw_filename = raw_filename.split( "?" ).shift();
612
+ }
613
+ const sanitized_filename = sanitize_upload_filename( raw_filename );
614
+ const final_filename = hash + "_" + sanitized_filename;
615
+ local_path += "/" + final_filename;
587
616
 
588
617
  D( "PUT: " + local_path );
589
618
 
@@ -618,7 +647,7 @@ function put_handler( req, res, next ) {
618
647
  if( responded ) return;
619
648
  I( "PUT: " + local_path );
620
649
  res.writeHead( 200, { "Content-Type": "application/json" } );
621
- res.write( o2j( { hash } ) );
650
+ res.write( o2j( { hash, filename: final_filename } ) );
622
651
  res.end();
623
652
  } );
624
653
 
@@ -795,7 +824,7 @@ function ws_attach( server, msg_handler ) {
795
824
  socket.send( o2j( msg ) );
796
825
  };
797
826
 
798
- const conn = { name, socket, send, cookies };
827
+ const conn = { name, socket, send, cookies, resource: wsreq.resource };
799
828
 
800
829
  socket.on( "error", function( err ) {
801
830
  E( "WS: error", err.stack || err );