kiro-mobile-bridge 1.0.7 → 1.0.10
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/README.md +16 -24
- package/package.json +1 -1
- package/src/public/index.html +1414 -1628
- package/src/routes/api.js +539 -0
- package/src/server.js +287 -2593
- package/src/services/cdp.js +210 -0
- package/src/services/click.js +533 -0
- package/src/services/message.js +214 -0
- package/src/services/snapshot.js +370 -0
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +34 -0
- package/src/utils/network.js +64 -0
- package/src/utils/security.js +160 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash utilities for content change detection
|
|
3
|
+
*
|
|
4
|
+
* NOTE: MD5 is used here for change detection only, NOT for security purposes.
|
|
5
|
+
* MD5 is fast and sufficient for detecting content changes in snapshots.
|
|
6
|
+
* Do NOT use these functions for password hashing, authentication, or any security-sensitive operations.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique ID from a string (e.g., WebSocket URL)
|
|
12
|
+
* Used for cascade identification, not security
|
|
13
|
+
* @param {string} input - String to hash
|
|
14
|
+
* @returns {string} - 8-character hash ID
|
|
15
|
+
*/
|
|
16
|
+
export function generateId(input) {
|
|
17
|
+
if (typeof input !== 'string' || !input) {
|
|
18
|
+
return crypto.randomBytes(4).toString('hex');
|
|
19
|
+
}
|
|
20
|
+
return crypto.createHash('md5').update(input).digest('hex').substring(0, 8);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compute MD5 hash for change detection
|
|
25
|
+
* Used to detect content changes in snapshots, not for security
|
|
26
|
+
* @param {string} content - Content to hash
|
|
27
|
+
* @returns {string} - Full MD5 hash
|
|
28
|
+
*/
|
|
29
|
+
export function computeHash(content) {
|
|
30
|
+
if (typeof content !== 'string') {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network utilities
|
|
3
|
+
*/
|
|
4
|
+
import { networkInterfaces } from 'os';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get local IP address for LAN access
|
|
8
|
+
* Returns the first non-internal IPv4 address found.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: On systems with multiple network interfaces, this returns the first one found.
|
|
11
|
+
* For more control, consider using environment variables or configuration.
|
|
12
|
+
*
|
|
13
|
+
* @returns {string} - Local IP or 'localhost' if no suitable interface found
|
|
14
|
+
*/
|
|
15
|
+
export function getLocalIP() {
|
|
16
|
+
const interfaces = networkInterfaces();
|
|
17
|
+
|
|
18
|
+
// Prioritize common interface names
|
|
19
|
+
const priorityInterfaces = ['eth0', 'en0', 'wlan0', 'Wi-Fi', 'Ethernet'];
|
|
20
|
+
|
|
21
|
+
// First, try priority interfaces
|
|
22
|
+
for (const name of priorityInterfaces) {
|
|
23
|
+
const ifaces = interfaces[name];
|
|
24
|
+
if (ifaces) {
|
|
25
|
+
for (const iface of ifaces) {
|
|
26
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
27
|
+
return iface.address;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback: return first non-internal IPv4
|
|
34
|
+
for (const name of Object.keys(interfaces)) {
|
|
35
|
+
for (const iface of interfaces[name]) {
|
|
36
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
37
|
+
return iface.address;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return 'localhost';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all available local IP addresses
|
|
47
|
+
* Useful for debugging or when user needs to choose interface
|
|
48
|
+
*
|
|
49
|
+
* @returns {Array<{name: string, address: string}>} - Array of interface names and addresses
|
|
50
|
+
*/
|
|
51
|
+
export function getAllLocalIPs() {
|
|
52
|
+
const interfaces = networkInterfaces();
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const name of Object.keys(interfaces)) {
|
|
56
|
+
for (const iface of interfaces[name]) {
|
|
57
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
58
|
+
results.push({ name, address: iface.address });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for input validation and sanitization
|
|
3
|
+
* Prevents path traversal, XSS, and other security vulnerabilities
|
|
4
|
+
*/
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate that a file path resolves within an allowed root directory
|
|
9
|
+
* Prevents path traversal attacks (e.g., ../../etc/passwd)
|
|
10
|
+
*
|
|
11
|
+
* @param {string} filePath - The file path to validate (can be relative or absolute)
|
|
12
|
+
* @param {string} rootDir - The allowed root directory
|
|
13
|
+
* @returns {{valid: boolean, resolvedPath: string|null, error: string|null}}
|
|
14
|
+
*/
|
|
15
|
+
export function validatePathWithinRoot(filePath, rootDir) {
|
|
16
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
17
|
+
return { valid: false, resolvedPath: null, error: 'Invalid file path' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!rootDir || typeof rootDir !== 'string') {
|
|
21
|
+
return { valid: false, resolvedPath: null, error: 'Invalid root directory' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Normalize and resolve both paths to absolute paths
|
|
26
|
+
const normalizedRoot = path.resolve(rootDir);
|
|
27
|
+
const resolvedPath = path.resolve(rootDir, filePath);
|
|
28
|
+
|
|
29
|
+
// Ensure the resolved path starts with the root directory
|
|
30
|
+
// Add path.sep to prevent matching partial directory names
|
|
31
|
+
// e.g., /home/user vs /home/username
|
|
32
|
+
const rootWithSep = normalizedRoot.endsWith(path.sep)
|
|
33
|
+
? normalizedRoot
|
|
34
|
+
: normalizedRoot + path.sep;
|
|
35
|
+
|
|
36
|
+
if (!resolvedPath.startsWith(rootWithSep) && resolvedPath !== normalizedRoot) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
resolvedPath: null,
|
|
40
|
+
error: 'Path traversal detected: path resolves outside allowed directory'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { valid: true, resolvedPath, error: null };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { valid: false, resolvedPath: null, error: `Path validation error: ${err.message}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Escape a string for safe inclusion in JavaScript code
|
|
52
|
+
* Handles all special characters that could break string literals or enable injection
|
|
53
|
+
*
|
|
54
|
+
* @param {string} str - The string to escape
|
|
55
|
+
* @returns {string} - Escaped string safe for JS inclusion
|
|
56
|
+
*/
|
|
57
|
+
export function escapeForJavaScript(str) {
|
|
58
|
+
if (typeof str !== 'string') {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return str
|
|
63
|
+
.replace(/\\/g, '\\\\') // Backslashes first (must be first!)
|
|
64
|
+
.replace(/'/g, "\\'") // Single quotes
|
|
65
|
+
.replace(/"/g, '\\"') // Double quotes
|
|
66
|
+
.replace(/`/g, '\\`') // Backticks (template literals)
|
|
67
|
+
.replace(/\$/g, '\\$') // Dollar signs (template literal interpolation)
|
|
68
|
+
.replace(/\n/g, '\\n') // Newlines
|
|
69
|
+
.replace(/\r/g, '\\r') // Carriage returns
|
|
70
|
+
.replace(/\t/g, '\\t') // Tabs
|
|
71
|
+
.replace(/\0/g, '\\0') // Null bytes
|
|
72
|
+
.replace(/\u2028/g, '\\u2028') // Line separator
|
|
73
|
+
.replace(/\u2029/g, '\\u2029'); // Paragraph separator
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate and sanitize click info object
|
|
78
|
+
* Ensures all properties are of expected types and within reasonable limits
|
|
79
|
+
*
|
|
80
|
+
* @param {object} clickInfo - The click info object to validate
|
|
81
|
+
* @returns {{valid: boolean, sanitized: object|null, error: string|null}}
|
|
82
|
+
*/
|
|
83
|
+
export function sanitizeClickInfo(clickInfo) {
|
|
84
|
+
if (!clickInfo || typeof clickInfo !== 'object') {
|
|
85
|
+
return { valid: false, sanitized: null, error: 'Click info must be an object' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sanitized = {};
|
|
89
|
+
|
|
90
|
+
// String properties with max length
|
|
91
|
+
const stringProps = [
|
|
92
|
+
{ name: 'tag', maxLength: 50 },
|
|
93
|
+
{ name: 'text', maxLength: 200 },
|
|
94
|
+
{ name: 'ariaLabel', maxLength: 200 },
|
|
95
|
+
{ name: 'role', maxLength: 50 },
|
|
96
|
+
{ name: 'className', maxLength: 500 },
|
|
97
|
+
{ name: 'tabLabel', maxLength: 100 },
|
|
98
|
+
{ name: 'parentTabLabel', maxLength: 100 },
|
|
99
|
+
{ name: 'filePath', maxLength: 500 },
|
|
100
|
+
{ name: 'toggleId', maxLength: 100 }
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const { name, maxLength } of stringProps) {
|
|
104
|
+
if (clickInfo[name] !== undefined) {
|
|
105
|
+
if (typeof clickInfo[name] !== 'string') {
|
|
106
|
+
sanitized[name] = String(clickInfo[name]).substring(0, maxLength);
|
|
107
|
+
} else {
|
|
108
|
+
sanitized[name] = clickInfo[name].substring(0, maxLength);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Boolean properties
|
|
114
|
+
const boolProps = [
|
|
115
|
+
'isTab', 'isCloseButton', 'isToggle', 'isModelSelector', 'isModelOption',
|
|
116
|
+
'isSendButton', 'isFileLink', 'isNotificationButton', 'isIconButton', 'isHistoryItem'
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const name of boolProps) {
|
|
120
|
+
if (clickInfo[name] !== undefined) {
|
|
121
|
+
sanitized[name] = Boolean(clickInfo[name]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { valid: true, sanitized, error: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validate message text for injection
|
|
130
|
+
*
|
|
131
|
+
* @param {string} message - The message to validate
|
|
132
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
133
|
+
*/
|
|
134
|
+
export function validateMessage(message) {
|
|
135
|
+
if (!message || typeof message !== 'string') {
|
|
136
|
+
return { valid: false, error: 'Message must be a non-empty string' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (message.length > 50000) {
|
|
140
|
+
return { valid: false, error: 'Message exceeds maximum length (50000 characters)' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { valid: true, error: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Sanitize a file path by removing null bytes and normalizing
|
|
148
|
+
* Does NOT validate path traversal - use validatePathWithinRoot for that
|
|
149
|
+
*
|
|
150
|
+
* @param {string} filePath - The file path to sanitize
|
|
151
|
+
* @returns {string} - Sanitized file path
|
|
152
|
+
*/
|
|
153
|
+
export function sanitizeFilePath(filePath) {
|
|
154
|
+
if (typeof filePath !== 'string') {
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remove null bytes which can be used to bypass security checks
|
|
159
|
+
return filePath.replace(/\0/g, '');
|
|
160
|
+
}
|