Files
nodecrypt/worker/index.js
T
2025-05-27 18:31:29 +08:00

529 lines
16 KiB
JavaScript

import { generateClientId, encryptMessage, decryptMessage, logEvent, isString, isObject, getTime } from './utils.js';
// 定义Content-Type映射
const contentTypeMap = {
'html': 'text/html',
'css': 'text/css',
'js': 'text/javascript',
'json': 'application/json',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'ico': 'image/x-icon'
};
// 获取文件的Content-Type
function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase();
return contentTypeMap[ext] || 'text/plain';
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;
// 处理WebSocket请求
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader && upgradeHeader === 'websocket') {
// Get or create a Durable Object instance
const id = env.CHAT_ROOM.idFromName('chat-room');
const stub = env.CHAT_ROOM.get(id);
return stub.fetch(request);
}
// 处理静态文件请求
try {
// 根路径重定向到index.html
if (path === '/' || path === '') {
return new Response('', {
status: 302,
headers: {
'Location': '/index.html'
}
});
}
// 从KV存储或Assets中获取静态文件
let contentPath = path;
if (contentPath.startsWith('/')) {
contentPath = contentPath.substring(1);
}
// 这里使用了Cloudflare Workers的__STATIC_CONTENT变量来获取静态资源
// 在实际部署时,您需要将静态文件上传到Cloudflare Workers的Assets中
const staticContent = await env.__STATIC_CONTENT.get(contentPath, 'stream');
if (staticContent === null) {
return new Response('Not Found', { status: 404 });
}
return new Response(staticContent, {
headers: {
'Content-Type': getContentType(contentPath)
}
});
} catch (error) {
return new Response('Server Error: ' + error.message, { status: 500 });
}
}
};
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; // 返回清理的连接数量
}
}