Files
nodecrypt/worker/index.js
T

480 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { generateClientId, encryptMessage, decryptMessage, logEvent, isString, isObject, getTime } from './utils.js';
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 处理WebSocket请求
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader && upgradeHeader === 'websocket') {
const id = env.CHAT_ROOM.idFromName('chat-room');
const stub = env.CHAT_ROOM.get(id);
return stub.fetch(request);
}
// 处理API请求
if (url.pathname.startsWith('/api/')) {
// ...API 逻辑...
return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json" } });
}
// 其余全部交给 ASSETS 处理(自动支持 hash 文件名和 SPA fallback)
return env.ASSETS.fetch(request);
}
};
export class ChatRoom { constructor(state, env) {
this.state = state;
// Use objects like original server.js instead of Maps
this.clients = {};
this.channels = {};
this.config = {
seenTimeout: 60000,
debug: false
};
// Initialize RSA key pair
this.initRSAKeyPair();
}
async initRSAKeyPair() {
try {
let stored = await this.state.storage.get('rsaKeyPair');
if (!stored) {
console.log('Generating new RSA keypair...');
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['sign', 'verify']
);
// 并行导出公钥和私钥以提高性能
const [publicKeyBuffer, privateKeyBuffer] = await Promise.all([
crypto.subtle.exportKey('spki', keyPair.publicKey),
crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
]);
stored = {
rsaPublic: btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer))),
rsaPrivateData: Array.from(new Uint8Array(privateKeyBuffer)),
createdAt: Date.now() // 记录密钥创建时间,用于后续判断是否需要轮换
};
await this.state.storage.put('rsaKeyPair', stored);
console.log('RSA key pair generated and stored');
}
// Reconstruct the private key
if (stored.rsaPrivateData) {
const privateKeyBuffer = new Uint8Array(stored.rsaPrivateData);
stored.rsaPrivate = await crypto.subtle.importKey(
'pkcs8',
privateKeyBuffer,
{
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256'
},
false,
['sign']
); }
this.keyPair = stored;
// 检查密钥是否需要轮换(如果已创建超过24小时)
if (stored.createdAt && (Date.now() - stored.createdAt > 24 * 60 * 60 * 1000)) {
// 如果没有任何客户端,则执行密钥轮换
if (Object.keys(this.clients).length === 0) {
console.log('密钥已使用24小时,进行轮换...');
await this.state.storage.delete('rsaKeyPair');
this.keyPair = null;
await this.initRSAKeyPair();
} else {
// 否则标记需要在客户端全部断开后进行轮换
await this.state.storage.put('pendingKeyRotation', true);
}
}
} catch (error) {
console.error('Error initializing RSA key pair:', error);
throw error;
}
}
async fetch(request) {
// Check for WebSocket upgrade
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket Upgrade', { status: 426 });
}
// Ensure RSA keys are initialized
if (!this.keyPair) {
await this.initRSAKeyPair();
}
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Accept the WebSocket connection
this.handleSession(server);
return new Response(null, {
status: 101,
webSocket: client,
});
} // WebSocket connection event handler
async handleSession(connection) { connection.accept();
// 清理旧连接
await this.cleanupOldConnections();
const clientId = generateClientId();
if (!clientId || this.clients[clientId]) {
this.closeConnection(connection);
return;
}
logEvent('connection', clientId, 'debug'); // Store client information
this.clients[clientId] = {
connection: connection,
seen: getTime(),
key: null,
shared: null,
channel: null
};
// Send RSA public key
try {
logEvent('sending-public-key', clientId, 'debug');
this.sendMessage(connection, JSON.stringify({
type: 'server-key',
key: this.keyPair.rsaPublic
}));
} catch (error) {
logEvent('sending-public-key', error, 'error');
} // Handle messages
connection.addEventListener('message', async (event) => {
const message = event.data;
if (!isString(message) || !this.clients[clientId]) {
return;
}
this.clients[clientId].seen = getTime();
if (message === 'ping') {
this.sendMessage(connection, 'pong');
return;
}
logEvent('message', [clientId, message], 'debug'); // Handle key exchange
if (!this.clients[clientId].shared && message.length < 2048) {
try {
// Generate ECDH key pair using P-384 curve (equivalent to secp384r1)
const keys = await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-384'
},
true,
['deriveBits', 'deriveKey']
);
const publicKeyBuffer = await crypto.subtle.exportKey('raw', keys.publicKey);
// Sign the public key using PKCS1 padding (compatible with original)
const signature = await crypto.subtle.sign(
{
name: 'RSASSA-PKCS1-v1_5'
},
this.keyPair.rsaPrivate,
publicKeyBuffer
);
// Convert hex string to Uint8Array for client public key
const clientPublicKeyHex = message;
const clientPublicKeyBytes = new Uint8Array(clientPublicKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// Import client's public key
const clientPublicKey = await crypto.subtle.importKey(
'raw',
clientPublicKeyBytes,
{ name: 'ECDH', namedCurve: 'P-384' },
false,
[]
);
// Derive shared secret bits (equivalent to computeSecret in Node.js)
const sharedSecretBits = await crypto.subtle.deriveBits(
{
name: 'ECDH',
public: clientPublicKey
},
keys.privateKey,
384 // P-384 produces 48 bytes (384 bits)
); // Take bytes 8-40 (32 bytes) for AES-256 key
this.clients[clientId].shared = new Uint8Array(sharedSecretBits).slice(8, 40);
const response = Array.from(new Uint8Array(publicKeyBuffer))
.map(b => b.toString(16).padStart(2, '0')).join('') +
'|' + btoa(String.fromCharCode(...new Uint8Array(signature)));
this.sendMessage(connection, response);
} catch (error) {
logEvent('message-key', [clientId, error], 'error');
this.closeConnection(connection);
}
return;
}
// Handle encrypted messages
if (this.clients[clientId].shared && message.length <= (8 * 1024 * 1024)) {
this.processEncryptedMessage(clientId, message);
}
}); // Handle connection close
connection.addEventListener('close', async (event) => {
logEvent('close', [clientId, event], 'debug');
const channel = this.clients[clientId].channel;
if (channel && this.channels[channel]) {
this.channels[channel].splice(this.channels[channel].indexOf(clientId), 1);
if (this.channels[channel].length === 0) {
delete(this.channels[channel]);
} else {
try {
const members = this.channels[channel];
for (const member of members) {
const client = this.clients[member]; if (this.isClientInChannel(client, channel)) {
this.sendMessage(client.connection, encryptMessage({
a: 'l',
p: members.filter((value) => {
return (value !== member ? true : false);
})
}, client.shared));
}
}
} catch (error) {
logEvent('close-list', [clientId, error], 'error');
}
}
}
if (this.clients[clientId]) {
delete(this.clients[clientId]);
}
});
}
// Process encrypted messages
processEncryptedMessage(clientId, message) {
let decrypted = null;
try {
decrypted = decryptMessage(message, this.clients[clientId].shared);
logEvent('message-decrypted', [clientId, decrypted], 'debug');
if (!isObject(decrypted) || !isString(decrypted.a)) {
return;
}
const action = decrypted.a;
if (action === 'j') {
this.handleJoinChannel(clientId, decrypted);
} else if (action === 'c') {
this.handleClientMessage(clientId, decrypted);
} else if (action === 'w') {
this.handleChannelMessage(clientId, decrypted);
}
} catch (error) {
logEvent('process-encrypted-message', [clientId, error], 'error');
} finally {
decrypted = null;
}
}
// Handle channel join requests
handleJoinChannel(clientId, decrypted) {
if (!isString(decrypted.p) || this.clients[clientId].channel) {
return;
}
try {
const channel = decrypted.p;
this.clients[clientId].channel = channel;
if (!this.channels[channel]) {
this.channels[channel] = [clientId];
} else {
this.channels[channel].push(clientId);
}
this.broadcastMemberList(channel);
} catch (error) {
logEvent('message-join', [clientId, error], 'error');
}
}
// Handle client messages
handleClientMessage(clientId, decrypted) {
if (!isString(decrypted.p) || !isString(decrypted.c) || !this.clients[clientId].channel) {
return;
}
try {
const channel = this.clients[clientId].channel;
const targetClient = this.clients[decrypted.c];
if (this.isClientInChannel(targetClient, channel)) {
const messageObj = {
a: 'c',
p: decrypted.p,
c: clientId
};
const encrypted = encryptMessage(messageObj, targetClient.shared);
this.sendMessage(targetClient.connection, encrypted);
messageObj.p = null;
}
} catch (error) {
logEvent('message-client', [clientId, error], 'error');
}
} // Handle channel messages
handleChannelMessage(clientId, decrypted) {
if (!isObject(decrypted.p) || !this.clients[clientId].channel) {
return;
}
try {
const channel = this.clients[clientId].channel;
// 过滤有效的目标成员
const validMembers = Object.keys(decrypted.p).filter(member => {
const targetClient = this.clients[member];
return isString(decrypted.p[member]) && this.isClientInChannel(targetClient, channel);
});
// 处理所有有效的目标成员
for (const member of validMembers) {
const targetClient = this.clients[member];
const messageObj = {
a: 'c',
p: decrypted.p[member],
c: clientId
}; const encrypted = encryptMessage(messageObj, targetClient.shared);
this.sendMessage(targetClient.connection, encrypted);
messageObj.p = null;
}
} catch (error) {
logEvent('message-channel', [clientId, error], 'error');
}
}
// Broadcast member list to channel
broadcastMemberList(channel) {
try {
const members = this.channels[channel];
for (const member of members) {
const client = this.clients[member];
if (this.isClientInChannel(client, channel)) {
const messageObj = {
a: 'l',
p: members.filter((value) => {
return (value !== member ? true : false);
})
};
const encrypted = encryptMessage(messageObj, client.shared);
this.sendMessage(client.connection, encrypted);
messageObj.p = null;
}
}
} catch (error) {
logEvent('broadcast-member-list', error, 'error');
}
} // Check if client is in channel
isClientInChannel(client, channel) {
return (
client &&
client.connection &&
client.shared &&
client.channel &&
client.channel === channel ?
true :
false
);
}
// Send message helper
sendMessage(connection, message) {
try {
// In Cloudflare Workers, WebSocket.READY_STATE_OPEN is 1
if (connection.readyState === 1) {
connection.send(message);
}
} catch (error) {
logEvent('sendMessage', error, 'error');
}
} // Close connection helper
closeConnection(connection) {
try {
connection.close(); } catch (error) {
logEvent('closeConnection', error, 'error');
}
}
// 连接清理方法
async cleanupOldConnections() {
const seenThreshold = getTime() - this.config.seenTimeout;
const clientsToRemove = [];
// 先收集需要移除的客户端,避免在迭代时修改对象
for (const clientId in this.clients) {
if (this.clients[clientId].seen < seenThreshold) {
clientsToRemove.push(clientId);
}
}
// 然后一次性移除所有过期客户端
for (const clientId of clientsToRemove) {
try {
logEvent('connection-seen', clientId, 'debug');
this.clients[clientId].connection.close();
delete this.clients[clientId];
} catch (error) {
logEvent('connection-seen', error, 'error'); }
}
// 如果没有任何客户端和房间,检查是否需要轮换密钥
if (Object.keys(this.clients).length === 0 && Object.keys(this.channels).length === 0) {
const pendingRotation = await this.state.storage.get('pendingKeyRotation');
if (pendingRotation) {
console.log('没有活跃客户端或房间,执行密钥轮换...');
await this.state.storage.delete('rsaKeyPair'); await this.state.storage.delete('pendingKeyRotation');
this.keyPair = null;
await this.initRSAKeyPair();
}
}
return clientsToRemove.length; // 返回清理的连接数量
}
}