添加本地docker部署方式
This commit is contained in:
1 parent
6b87b63bd0
commit
f8f9a52e78
6 files changed
+832
-2
No files matched your search
Generated
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "nodecrypt-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nodecrypt-server",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2",
|
||||
"ws": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
|
||||
"integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "nodecrypt-server",
|
||||
"version": "1.0.0",
|
||||
"description": "NodeCrypt Server",
|
||||
"main": "server.js",
|
||||
"author": "Zoltan Hajdu",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ws": "^7.3.1",
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
#!/usr/bin/env nodejs
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const ws = require('ws');
|
||||
|
||||
// Generate a new RSA key pair
|
||||
// 生成一个新的 RSA 密钥对
|
||||
const generateRSAKeyPair = () => {
|
||||
try {
|
||||
console.log('Generating new RSA keypair...');
|
||||
const {
|
||||
publicKey,
|
||||
privateKey
|
||||
} = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'der'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'der'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
rsaPublic: Buffer.from(publicKey).toString('base64'),
|
||||
rsaPrivate: crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: 'der',
|
||||
type: 'pkcs8'
|
||||
})
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating RSA key pair:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const keyPair = generateRSAKeyPair();
|
||||
console.log('RSA key pair generated successfully');
|
||||
|
||||
const config = {
|
||||
rsaPrivate: keyPair.rsaPrivate,
|
||||
rsaPublic: keyPair.rsaPublic,
|
||||
wsHost: '127.0.0.1',
|
||||
wsPort: 8088,
|
||||
seenTimeout: 60000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
|
||||
const wss = new ws.Server({
|
||||
host: config.wsHost,
|
||||
port: config.wsPort,
|
||||
perMessageDeflate: false
|
||||
});
|
||||
|
||||
console.log('server started', config.wsHost, config.wsPort);
|
||||
|
||||
|
||||
var clients = {};
|
||||
var channels = {};
|
||||
|
||||
// WebSocket server connection event handler
|
||||
// WebSocket 服务器连接事件处理程序
|
||||
wss.on('connection', (connection) => {
|
||||
|
||||
if (
|
||||
!connection
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const seenThreshold = (getTime() - config.seenTimeout);
|
||||
|
||||
for (
|
||||
const clientId in clients
|
||||
) {
|
||||
if (
|
||||
clients[clientId].seen < seenThreshold
|
||||
) {
|
||||
try {
|
||||
logEvent('connection-seen', clientId, 'debug');
|
||||
clients[clientId].connection.terminate();
|
||||
} catch (error) {
|
||||
logEvent('connection-seen', error, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const clientId = generateClientId();
|
||||
|
||||
if (
|
||||
!clientId ||
|
||||
clients[clientId]
|
||||
) {
|
||||
closeConnection(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent('connection', clientId, 'debug');
|
||||
|
||||
|
||||
clients[clientId] = {
|
||||
connection: connection,
|
||||
seen: getTime(),
|
||||
key: null,
|
||||
channel: null
|
||||
};
|
||||
|
||||
try {
|
||||
logEvent('sending-public-key', clientId, 'debug');
|
||||
sendMessage(connection, JSON.stringify({
|
||||
type: 'server-key',
|
||||
key: config.rsaPublic
|
||||
}));
|
||||
} catch (error) {
|
||||
logEvent('sending-public-key', error, 'error');
|
||||
}
|
||||
|
||||
|
||||
|
||||
connection.on('message', (message) => {
|
||||
|
||||
if (
|
||||
!isString(message) ||
|
||||
!clients[clientId]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
clients[clientId].seen = getTime();
|
||||
|
||||
if (
|
||||
message === 'ping'
|
||||
) {
|
||||
sendMessage(connection, 'pong');
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent('message', [clientId, message], 'debug');
|
||||
|
||||
if (
|
||||
!clients[clientId].shared &&
|
||||
message.length < 2048
|
||||
) {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const keys = crypto.createECDH('secp384r1');
|
||||
|
||||
keys.generateKeys();
|
||||
|
||||
const publicKey = keys.getPublicKey();
|
||||
const signature = crypto.sign('sha256', publicKey, {
|
||||
key: config.rsaPrivate,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING,
|
||||
dsaEncoding: 'ieee-p1363'
|
||||
});
|
||||
|
||||
clients[clientId].shared = keys.computeSecret(message, 'hex', null).slice(8, 40);
|
||||
|
||||
sendMessage(connection, publicKey.toString('hex') + '|' + signature.toString('base64'));
|
||||
|
||||
} catch (error) {
|
||||
logEvent('message-key', [clientId, error], 'error');
|
||||
closeConnection(connection);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (
|
||||
clients[clientId].shared &&
|
||||
message.length <= (8 * 1024 * 1024)
|
||||
) {
|
||||
|
||||
processEncryptedMessage(clientId, message);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
connection.on('close', (event) => {
|
||||
|
||||
logEvent('close', [clientId, event], 'debug');
|
||||
|
||||
|
||||
const channel = clients[clientId].channel;
|
||||
|
||||
if (
|
||||
channel &&
|
||||
channels[channel]
|
||||
) {
|
||||
|
||||
channels[channel].splice(channels[channel].indexOf(clientId), 1);
|
||||
|
||||
if (
|
||||
channels[channel].length === 0
|
||||
) {
|
||||
|
||||
|
||||
delete(channels[channel]);
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const members = channels[channel];
|
||||
|
||||
for (
|
||||
const member of members
|
||||
) {
|
||||
|
||||
const client = clients[member];
|
||||
|
||||
if (
|
||||
isClientInChannel(client, channel)
|
||||
) {
|
||||
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 (
|
||||
clients[clientId]
|
||||
) {
|
||||
delete(clients[clientId]);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Process encrypted messages
|
||||
// 处理加密消息
|
||||
const processEncryptedMessage = (clientId, message) => {
|
||||
let decrypted = null;
|
||||
|
||||
try {
|
||||
decrypted = decryptMessage(message, clients[clientId].shared);
|
||||
|
||||
logEvent('message-decrypted', [clientId, decrypted], 'debug');
|
||||
|
||||
if (
|
||||
!isObject(decrypted) ||
|
||||
!isString(decrypted.a)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = decrypted.a;
|
||||
|
||||
if (action === 'j') {
|
||||
handleJoinChannel(clientId, decrypted);
|
||||
} else if (action === 'c') {
|
||||
handleClientMessage(clientId, decrypted);
|
||||
} else if (action === 'w') {
|
||||
handleChannelMessage(clientId, decrypted);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logEvent('process-encrypted-message', [clientId, error], 'error');
|
||||
} finally {
|
||||
decrypted = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle channel join requests
|
||||
// 处理加入频道请求
|
||||
const handleJoinChannel = (clientId, decrypted) => {
|
||||
if (
|
||||
!isString(decrypted.p) ||
|
||||
clients[clientId].channel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = decrypted.p;
|
||||
|
||||
clients[clientId].channel = channel;
|
||||
|
||||
if (!channels[channel]) {
|
||||
channels[channel] = [clientId];
|
||||
} else {
|
||||
channels[channel].push(clientId);
|
||||
}
|
||||
|
||||
broadcastMemberList(channel);
|
||||
|
||||
} catch (error) {
|
||||
logEvent('message-join', [clientId, error], 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle client messages
|
||||
// 处理客户端消息
|
||||
const handleClientMessage = (clientId, decrypted) => {
|
||||
if (
|
||||
!isString(decrypted.p) ||
|
||||
!isString(decrypted.c) ||
|
||||
!clients[clientId].channel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = clients[clientId].channel;
|
||||
const targetClient = clients[decrypted.c];
|
||||
|
||||
if (isClientInChannel(targetClient, channel)) {
|
||||
const messageObj = {
|
||||
a: 'c',
|
||||
p: decrypted.p,
|
||||
c: clientId
|
||||
};
|
||||
|
||||
const encrypted = encryptMessage(messageObj, targetClient.shared);
|
||||
sendMessage(targetClient.connection, encrypted);
|
||||
|
||||
messageObj.p = null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logEvent('message-client', [clientId, error], 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle channel messages
|
||||
// 处理频道消息
|
||||
const handleChannelMessage = (clientId, decrypted) => {
|
||||
if (
|
||||
!isObject(decrypted.p) ||
|
||||
!clients[clientId].channel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = clients[clientId].channel;
|
||||
|
||||
for (const member in decrypted.p) {
|
||||
const targetClient = clients[member];
|
||||
|
||||
if (
|
||||
isString(decrypted.p[member]) &&
|
||||
isClientInChannel(targetClient, channel)
|
||||
) {
|
||||
const messageObj = {
|
||||
a: 'c',
|
||||
p: decrypted.p[member],
|
||||
c: clientId
|
||||
};
|
||||
|
||||
const encrypted = encryptMessage(messageObj, targetClient.shared);
|
||||
sendMessage(targetClient.connection, encrypted);
|
||||
|
||||
messageObj.p = null;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logEvent('message-channel', [clientId, error], 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Broadcast member list to channel
|
||||
// 向频道广播成员列表
|
||||
const broadcastMemberList = (channel) => {
|
||||
try {
|
||||
const members = channels[channel];
|
||||
|
||||
for (const member of members) {
|
||||
const client = clients[member];
|
||||
|
||||
if (isClientInChannel(client, channel)) {
|
||||
const filteredMembers = members.filter(value => value !== member);
|
||||
|
||||
const listObj = {
|
||||
a: 'l',
|
||||
p: filteredMembers
|
||||
};
|
||||
|
||||
const encrypted = encryptMessage(listObj, client.shared);
|
||||
sendMessage(client.connection, encrypted);
|
||||
|
||||
listObj.p = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logEvent('broadcast-member-list', error, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Log events with timestamps and levels
|
||||
// 记录带时间戳和级别的事件
|
||||
const logEvent = (source, message, level) => {
|
||||
if (
|
||||
level !== 'debug' ||
|
||||
config.debug
|
||||
) {
|
||||
|
||||
const date = new Date(),
|
||||
dateString = date.getFullYear() + '-' +
|
||||
('0' + (date.getMonth() + 1)).slice(-2) + '-' +
|
||||
('0' + date.getDate()).slice(-2) + ' ' +
|
||||
('0' + date.getHours()).slice(-2) + ':' +
|
||||
('0' + date.getMinutes()).slice(-2) + ':' +
|
||||
('0' + date.getSeconds()).slice(-2);
|
||||
|
||||
console.log('[' + dateString + ']', (level ? level.toUpperCase() : 'INFO'), source + (message ? ':' : ''), (message ? message : ''));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const generateClientId = () => {
|
||||
try {
|
||||
return (crypto.randomBytes(8).toString('hex'));
|
||||
} catch (error) {
|
||||
logEvent('generateClientId', error, 'error');
|
||||
return (null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const closeConnection = (connection) => {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (error) {
|
||||
logEvent('closeConnection', error, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const isClientInChannel = (client, channel) => {
|
||||
return (
|
||||
client &&
|
||||
client.connection &&
|
||||
client.shared &&
|
||||
client.channel &&
|
||||
client.channel === channel ?
|
||||
true :
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const sendMessage = (connection, message) => {
|
||||
try {
|
||||
if (
|
||||
connection.readyState &&
|
||||
connection.readyState === ws.OPEN
|
||||
) {
|
||||
connection.send(message);
|
||||
}
|
||||
} catch (error) {
|
||||
logEvent('sendMessage', error, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const encryptMessage = (message, key) => {
|
||||
|
||||
let encrypted = '';
|
||||
|
||||
try {
|
||||
|
||||
const messageBuffer = Buffer.from(JSON.stringify(message), 'utf8');
|
||||
|
||||
const paddedBuffer = (messageBuffer.length % 16) !== 0 ?
|
||||
Buffer.concat([messageBuffer, Buffer.alloc(16 - (messageBuffer.length % 16))]) :
|
||||
messageBuffer;
|
||||
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
cipher.setAutoPadding(false);
|
||||
|
||||
encrypted = iv.toString('base64') + '|' + cipher.update(paddedBuffer, '', 'base64') + cipher.final('base64');
|
||||
|
||||
} catch (error) {
|
||||
logEvent('encryptMessage', error, 'error');
|
||||
}
|
||||
|
||||
return (encrypted);
|
||||
|
||||
};
|
||||
|
||||
|
||||
const decryptMessage = (message, key) => {
|
||||
|
||||
let decrypted = {};
|
||||
|
||||
try {
|
||||
|
||||
const parts = message.split('|');
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'aes-256-cbc',
|
||||
key,
|
||||
Buffer.from(parts[0], 'base64')
|
||||
);
|
||||
|
||||
decipher.setAutoPadding(false);
|
||||
|
||||
const decryptedText = decipher.update(parts[1], 'base64', 'utf8') + decipher.final('utf8');
|
||||
decrypted = JSON.parse(decryptedText.replace(/\0+$/, ''));
|
||||
|
||||
} catch (error) {
|
||||
logEvent('decryptMessage', error, 'error');
|
||||
}
|
||||
|
||||
return (decrypted);
|
||||
|
||||
};
|
||||
|
||||
|
||||
const getTime = () => {
|
||||
return (new Date().getTime());
|
||||
};
|
||||
|
||||
|
||||
const isString = (value) => {
|
||||
return (
|
||||
value &&
|
||||
Object.prototype.toString.call(value) === '[object String]' ?
|
||||
true :
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const isArray = (value) => {
|
||||
return (
|
||||
value &&
|
||||
Object.prototype.toString.call(value) === '[object Array]' ?
|
||||
true :
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const isObject = (value) => {
|
||||
return (
|
||||
value &&
|
||||
Object.prototype.toString.call(value) === '[object Object]' ?
|
||||
true :
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
}, 30000);
|
||||
Reference in new issue
Block a user