首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >节点websockets

节点websockets
EN

Code Review用户
提问于 2019-04-13 12:01:32
回答 1查看 197关注 0票数 5

我有这个代码,我用它做两件事

  1. 连接到lxd实例,并将通过socket.io接收到的任何操作事件发送到客户端。
  2. 终端客户端与lxd之间的桥梁

它写得很糟糕,所以任何指针都会很棒(但是它很好)

代码语言:javascript
复制
// This originated from https://gist.github.com/CalebEverett/bed94582b437ffe88f650819d772b682
// and was modified to suite our needs
const fs = require('fs'),
    WebSocket = require('ws'),
    express = require('express'),
    https = require('https'),
    mysql = require('mysql'),
    expressWs = require('express-ws'),
    path = require('path'),
    cors = require('cors');

const envImportResult = require('dotenv').config({
    path: "/var/www/LxdMosaic/.env"
});

if (envImportResult.error) {
    throw envImportResult.error
}

// Https certificate and key file location for secure websockets + https server
var privateKey = fs.readFileSync(process.env.CERT_PRIVATE_KEY, 'utf8'),
    certificate = fs.readFileSync(process.env.CERT_PATH, 'utf8');
    certDir = "/var/www/LxdMosaic/src/sensitiveData/certs/",
    lxdConsoles = [],
    credentials = {
        key: privateKey,
        cert: certificate
    },
    app = express();

app.use(cors());
var bodyParser = require('body-parser')
app.use( bodyParser.json() );       // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({     // to support URL-encoded bodies
  extended: true
}));

var httpsServer = https.createServer(credentials, app);
var io = require('socket.io')(httpsServer);

var operationSocket = io.of("/operations")
// expressWs(app, httpsServer);

var con = mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME
});

var hostDetails = {};

function createExecOptions(host, container) {
    return {
        method: 'POST',
        host: hostDetails[host].hostWithOutProtoOrPort,
        port: hostDetails[host].port,
        path: '/1.0/containers/' + container + '/exec',
        cert: fs.readFileSync(hostDetails[host].cert),
        key: fs.readFileSync(hostDetails[host].key),
        rejectUnauthorized: false
    }
}

const lxdExecBody = JSON.stringify({
    "command": ["bash"],
    "environment": {
        "HOME": "/root",
        "TERM": "xterm",
        "USER": "root"
    },
    "wait-for-websocket": true,
    "interactive": true,
})


con.connect(function(err) {
    if (err) {
        throw err;
    }
});

function createWebSockets() {
    con.query("SELECT * FROM Hosts", function(err, result, fields) {
        if (err) {
            throw err;
        }
        for (i = 0; i < result.length; i++) {
            let lxdClientCert = certDir + result[i].Host_Cert_Only_File
            let lxdClientKey = certDir + result[i].Host_Key_File

            if(result[i].Host_Online == 0){
                continue;
            }

            // Connecting to the lxd server/s
            const wsoptions = {
                cert: fs.readFileSync(lxdClientCert),
                key: fs.readFileSync(lxdClientKey),
                rejectUnauthorized: false,
            }

            var portRegex = /:[0-9]+/;

            let stringUrl = result[i].Host_Url_And_Port;
            let urlURL = new URL(result[i].Host_Url_And_Port);

            let hostWithOutProto = stringUrl.replace("https://", "");
            let hostWithOutProtoOrPort = hostWithOutProto.replace(portRegex, "");

            hostDetails[result[i].Host_Url_And_Port] = {
                cert: lxdClientCert,
                key: lxdClientKey,
                hostWithOutProtoOrPort: hostWithOutProtoOrPort,
                port: urlURL.port
            };

            var ws = new WebSocket('wss://' + hostWithOutProto + '/1.0/events?type=operation', wsoptions);

            ws.on('message', function(data, flags) {
                var buf = Buffer.from(data)
                let message = JSON.parse(data.toString());
                message.host = hostWithOutProtoOrPort;
                operationSocket.emit('operationUpdate', message);
            });
        }
    });
}


httpsServer.listen(3000, function() {});


app.get('/hosts/reload/', function(req, res) {
    createWebSockets();
    res.send({
        success: "reloaded"
    });
});

app.post('/hosts/message/', function(req, res) {
    console.log(req.body.data);
    operationSocket.emit(req.body.type, req.body.data);
    res.send({
        success: "delivered"
    });
});

app.get('/', function(req, res) {
    res.sendFile(path.join(__dirname + '/index.html'));
});

var terminalsIo = io.of("/terminals");

terminalsIo.on("connect", function(socket) {

    let indentifier = socket.handshake.query.pid;

    if(lxdConsoles[indentifier] == undefined) {
        let host = socket.handshake.query.host;
        let container = socket.handshake.query.container;


        let execOptions = createExecOptions(host, container);

        const wsoptions = {
            cert: execOptions.cert,
            key: execOptions.key,
            rejectUnauthorized: false
        }

        const lxdReq = https.request(execOptions, res => {
            res.on('data', d => {

                const output = JSON.parse(d);

                if(output.hasOwnProperty("error") && output.error !== ""){
                    socket.emit("data", "Container Offline");
                    return false;
                }

                const lxdWs = new WebSocket('wss://' +
                    execOptions.host + ':' + execOptions.port + output.operation +
                    '/websocket?secret=' + output.metadata.metadata.fds['0'],
                    wsoptions
                );

                lxdWs.on('error', error => console.log(error));

                lxdWs.on('message', data => {
                    try {
                        const buf = Buffer.from(data);
                        data = buf.toString();
                        socket.emit("data", data);
                    } catch (ex) {
                        // The WebSocket is not open, ignore
                    }
                });
                lxdConsoles.push(lxdWs);
            });
        });
        lxdReq.write(lxdExecBody);
        lxdReq.end();
    }

    //NOTE When user inputs from browser
    socket.on('data', function(msg) {
        lxdConsoles[indentifier].send(msg, {
            binary: true
        }, () => {});
    });

    socket.on('close', function(indentifier) {
        setTimeout(() => {
            if(lxdConsoles[indentifier] == undefined){
                return
            }
             lxdConsoles[indentifier].send('exit  \r', { binary: true }, function(){
                lxdConsoles[indentifier].close();
                delete lxdConsoles[indentifier];
            });
         }, 100);
    });
});

app.post('/terminals', function(req, res) {
    // Create a indentifier for the console, this should allow multiple consolses
    // per user
    res.send(lxdConsoles.length.toString());
});


createWebSockets();

(这来自我维护这里的一个开源项目,所以如果您给出了一个答案,请注意它可能会在那里使用)

EN

回答 1

Code Review用户

回答已采纳

发布于 2019-08-21 06:39:03

在节点中,抛出错误而不捕获它将终止进程。这似乎是主要的错误处理模式(基于,您似乎依赖于pm2来重新启动进程)。这意味着,即使是一个客户端也会触发代码中的错误,每个客户端都将被断开连接。解决这一问题需要重写大量代码以正确处理异步错误--例如,如果DB查询错误,createWebSockets返回承诺并拒绝该承诺,然后在/hosts/reload/中处理该错误并将错误传递给客户端。

createWebSockets()在循环中调用fs.readFileSync --如果有大量的主机,这可能导致整个服务器在每个主机重新读取证书/键时冻结。您可以考虑已经读取的缓存键(所以您不只是在每次重新加载时重新读取相同的键)和/或使用异步fs.readFile API。(如果您熟悉承诺,可以使用异步/等待和fs.promises.readFile来保持类似的代码结构)

/hosts/reload/听起来似乎是为了重新加载主机列表而多次运行,但是createWebSockets()处理重新加载的方式与最初的设置没有任何不同。这有几个问题:

  • 如果从DB中删除主机,hostDetails仍将包括有关主机的信息。
  • 安装了events?type=operation websockets,但随后抛出了它们的句柄。因此,不仅错误和主机删除没有处理,而且还将为上一次重新加载期间已经存在的主机创建一个新的websocket。这意味着将有多个websockets连接到单个主机,其侦听器仍在转发消息。这不仅是内存泄漏,还意味着将有重复的operationUpdate消息被发送到operationSocket上。
    • 类似地,像这些长寿命的套接字通常都有错误处理和重试逻辑。

if(lxdConsoles[indentifier] == undefined) {

res.send(lxdConsoles.length.toString());

看起来,lxdConsoles.length被用作唯一标识符(这里) --但如果两个客户端同时尝试并打开控制台,这并不能保证工作。可能会发生这样的情况:它们都请求POST /terminals,获得相同的id,然后都尝试将相同的id传递回websocket。

一个简单的解决方法是将lxdConsoles转换为对象而不是数组,让POST /terminals处理程序只返回一个uuid而不是lxdConsoles.length。虽然服务器根本没有对令牌进行身份验证,但我们最好让客户机生成uuid。

res.on('data', d => {不是从HTTP请求读取响应数据的正确方式。data事件表示已经接收到单个数据块,而不是完整的消息。从技术上讲,这段代码需要一个缓冲区,存储所有数据块,然后在end事件上处理整个缓冲区。

大致如下:

代码语言:javascript
复制
let body = '';
res.on('data', d => body += d.toString('utf-8'));
res.on('end', () => { /* do something with the complete "body" */ });

或者使用一个更简单的HTTP库,比如请求-承诺

基于LXD文档,返回的消息保证很小.但这段代码仍然很脆弱(如果LXD曾经更改以返回更多数据,或者如果URL更改为返回更多数据的内容,这可能会中断)。

代码语言:javascript
复制
 try {
     const buf = Buffer.from(data);
     data = buf.toString();
     socket.emit("data", data);
 } catch (ex) {
     // The WebSocket is not open, ignore
 }

这是关于: try/catch被用来在没有记录错误的情况下丢弃错误(因此用户可能有神秘的行为,甚至看不到日志中的原因),而且注释读起来很奇怪:这里有两个websockets (客户机socket和服务器lxdWs,所以注释应该指定哪个错误,并描述错误不需要处理的原因)

代码语言:javascript
复制
socket.on('close', function(indentifier) {
    setTimeout(() => {
        // ...
     }, 100);
});

目前还不清楚setTimeout在这里做什么--如果它需要用于特定的时间安排,那么发表评论将是个好主意。否则你可以移除它。

小风格的东西:

  • 代码具有不一致的空格、引用、var/const用法和分号。只需使用更漂亮,不要再担心它了。
  • indentifier拼错了。
票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/217391

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档