All files / server utils.js

100% Statements 53/53
93.75% Branches 30/32
100% Functions 7/7
100% Lines 52/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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  6x 2x                             2x     6x 17x         6x   6x 6x       6x 4x             4x 4x 12x 8x   4x     4x         6x 29x 2x   27x         27x     6x 18x 3x   15x 15x 2x 2x 2x       15x 8x 8x 3x   5x 5x     7x 6x 6x 4x 4x 4x 4x 4x 4x   2x     6x 6x 1x     5x 20x   5x   1x                      
// Usage flags
const printHelp = () => {
	console.log(`teXt0wnz backend server
Usage: {bun,node} server [port] [options]
 
Options:
  --ssl                 Enable SSL (requires certificates in ssl-dir)
  --ssl-dir <path>      SSL certificate directory (default: /etc/ssl/private)
  --save-interval <min> Auto-save interval in minutes (default: 30)
  --session-name <name> Session file prefix (default: joint)
  --debug               Enable verbose console messages
  --help                Show this help message
 
Examples:
  bun server 8080 --ssl --session-name myart --debug
  node server --save-interval 60 --session-name collaborative
`);
	process.exit(0);
};
 
const callout = msg => {
	console.log(
		`╓─────  ${sanitize(msg, 100, false)}\n╙───────────────────────────────── ─ ─`,
	);
};
 
const createTimestampedFilename = (sessionName, extension) => {
	// Windows safe file names
	const timestamp = new Date().toISOString().replace(/[:]/g, '-');
	return `${sessionName}-${timestamp}.${extension}`;
};
 
// Strips possibly sensitive headers
const cleanHeaders = headers => {
	const SENSITIVE_HEADERS = [
		'authorization',
		'cookie',
		'set-cookie',
		'proxy-authorization',
		'x-api-key',
	];
	const redacted = {};
	for (const [key, value] of Object.entries(headers)) {
		if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
			redacted[key] = '[REDACTED]';
		} else {
			redacted[key] = value;
		}
	}
	return redacted;
};
 
// Strips Unicode control characters and newlines,
// limits length, and optionally adds quotes
const sanitize = (input, limit = 100, quote = true) => {
	if (input === null || input === undefined) {
		return '';
	}
	const str = String(input)
		.trim()
		.replace(/\p{C}/gu, '')
		.replace(/[\n\r]/g, '')
		.substring(0, limit);
	return quote ? `'${str}'` : str;
};
 
const anonymizeIp = ip => {
	if (!ip) {
		return 'unknown';
	}
	let normalizedIp = ip;
	if (normalizedIp.includes('::ffff:')) {
		const ipv4Mapped = normalizedIp.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
		Eif (ipv4Mapped) {
			normalizedIp = ipv4Mapped[1];
		}
	}
	// Mask the final octet for IPv4
	if (normalizedIp.includes('.')) {
		const parts = normalizedIp.split('.');
		if (parts.length !== 4) {
			return 'invalid ip';
		}
		parts[3] = 'X';
		return parts.join('.');
	}
	// Handle IPv6 (including compressed notation)
	if (normalizedIp.includes(':')) {
		const expandIPv6 = address => {
			if (address.includes('::')) {
				const [head, tail] = address.split('::', 2);
				const headParts = head ? head.split(':') : [];
				const tailParts = tail ? tail.split(':') : [];
				const missing = 8 - (headParts.length + tailParts.length);
				const zeros = Array(missing > 0 ? missing : 0).fill('0');
				return [...headParts, ...zeros, ...tailParts];
			} else {
				return address.split(':');
			}
		};
		const parts = expandIPv6(normalizedIp);
		if (parts.length !== 8) {
			return 'invalid ip';
		}
		// Mask the last 4 segments
		for (let i = 4; i < 8; i++) {
			parts[i] = 'X';
		}
		return parts.join(':');
	}
	return 'unknown';
};
 
export {
	printHelp,
	callout,
	createTimestampedFilename,
	cleanHeaders,
	sanitize,
	anonymizeIp,
};