+
10
-

回答

在Node.js中实现类似PHP FastCGI的多租户虚拟空间需要考虑进程隔离、文件系统隔离和资源限制。以下是完整的实现方案:

1. 主控制器(Master Process)

// master.js
const cluster = require('cluster');
const fs = require('fs');
const path = require('path');
const http = require('http');
const { Worker } = require('worker_threads');

class VirtualHostManager {
    constructor() {
        this.tenants = new Map();
        this.workers = new Map();
    }

    // 注册租户
    registerTenant(tenantId, config) {
        this.tenants.set(tenantId, {
            id: tenantId,
            rootDir: config.rootDir,
            uid: config.uid,
            gid: config.gid,
            maxMemory: config.maxMemory || 128 * 1024 * 1024, // 128MB
            maxCpu: config.maxCpu || 0.5,
            domain: config.domain,
            port: config.port
        });
    }

    // 为每个租户创建隔离的工作进程
    spawnTenantWorker(tenantId) {
        const tenant = this.tenants.get(tenantId);
        if (!tenant) return;

        if (cluster.isMaster) {
            const worker = cluster.fork({
                TENANT_ID: tenantId,
                TENANT_ROOT: tenant.rootDir,
                TENANT_UID: tenant.uid,
                TENANT_GID: tenant.gid,
                NODE_OPTIONS: `--max-old-space-size=${Math.floor(tenant.maxMemory / 1024 / 1024)}`
            });

            this.workers.set(tenantId, worker);

            worker.on('exit', (code, signal) => {
                console.log(`Tenant ${tenantId} worker died, restarting...`);
                this.spawnTenantWorker(tenantId);
            });
        }
    }
}

2. 沙箱环境(Sandbox)

// sandbox.js
const vm = require('vm');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

class TenantSandbox {
    constructor(tenantConfig) {
        this.tenantId = tenantConfig.id;
        this.rootDir = tenantConfig.rootDir;
        this.uid = tenantConfig.uid;
        this.gid = tenantConfig.gid;
        this.context = this.createContext();
    }

    // 创建隔离的执行上下文
    createContext() {
        const sandbox = {
            console: console,
            setTimeout: setTimeout,
            setInterval: setInterval,
            clearTimeout: clearTimeout,
            clearInterval: clearInterval,
            Buffer: Buffer,
            process: {
                env: {},
                version: process.version,
                platform: process.platform,
                arch: process.arch,
                cwd: () => this.rootDir,
                memoryUsage: process.memoryUsage
            },
            require: this.createSafeRequire(),
            __dirname: this.rootDir,
            __filename: ''
        };

        // 添加安全的文件系统API
        sandbox.fs = this.createSafeFS();

        return vm.createContext(sandbox);
    }

    // 创建受限的require函数
    createSafeRequire() {
        const allowedModules = ['http', 'https', 'url', 'querystring', 'crypto'];

        return (moduleName) => {
            // 只允许加载白名单模块
            if (allowedModules.includes(moduleName)) {
                return require(moduleName);
            }

            // 检查是否是相对路径模块
            if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
                const modulePath = path.resolve(this.rootDir, moduleName);

                // 确保模块在租户目录内
                if (!modulePath.startsWith(this.rootDir)) {
                    throw new Error(`Access denied: ${moduleName}`);
                }

                return require(modulePath);
            }

            throw new Error(`Module not allowed: ${moduleName}`);
        };
    }

    // 创建受限的文件系统API
    createSafeFS() {
        const safeFS = {};
        const self = this;

        // 包装fs方法,限制在租户目录内
        const wrapFSMethod = (method) => {
            return (...args) => {
                // 检查路径参数
                if (args[0] && typeof args[0] === 'string') {
                    const resolvedPath = path.resolve(self.rootDir, args[0]);

                    // 确保路径在租户目录内
                    if (!resolvedPath.startsWith(self.rootDir)) {
                        throw new Error(`Access denied: ${args[0]}`);
                    }

                    args[0] = resolvedPath;
                }

                return fs[method](...args);
            };
        };

        // 包装常用的fs方法
        ['readFile', 'writeFile', 'readdir', 'stat', 'mkdir', 'rmdir', 'unlink']
            .forEach(method => {
                safeFS[method] = wrapFSMethod(method);
                safeFS[method + 'Sync'] = wrapFSMethod(method + 'Sync');
            });

        return safeFS;
    }

    // 执行租户代码
    async execute(code, filename = 'index.js') {
        try {
            // 设置进程用户和组(需要root权限)
            if (process.platform !== 'win32' && process.getuid() === 0) {
                process.setgid(this.gid);
                process.setuid(this.uid);
            }

            const script = new vm.Script(code, {
                filename: path.join(this.rootDir, filename),
                timeout: 30000, // 30秒超时
                displayErrors: true
            });

            return script.runInContext(this.context);
        } catch (error) {
            console.error(`Sandbox execution error for tenant ${this.tenantId}:`, error);
            throw error;
        }
    }
}

module.exports = TenantSandbox;

3. 工作进程(Worker Process)

// worker.js
const http = require('http');
const express = require('express');
const TenantSandbox = require('./sandbox');

if (!cluster.isMaster) {
    const tenantId = process.env.TENANT_ID;
    const tenantRoot = process.env.TENANT_ROOT;
    const tenantUid = parseInt(process.env.TENANT_UID);
    const tenantGid = parseInt(process.env.TENANT_GID);

    // 创建租户沙箱
    const sandbox = new TenantSandbox({
        id: tenantId,
        rootDir: tenantRoot,
        uid: tenantUid,
        gid: tenantGid
    });

    // 创建Express应用
    const app = express();

    // 中间件:检查请求路径
    app.use((req, res, next) => {
        // 确保请求路径在租户目录内
        const requestPath = path.join(tenantRoot, req.path);
        if (!requestPath.startsWith(tenantRoot)) {
            return res.status(403).send('Access Denied');
        }
        next();
    });

    // 处理静态文件
    app.use(express.static(tenantRoot, {
        dotfiles: 'deny',
        index: ['index.js', 'index.html']
    }));

    // 处理Node.js脚本执行
    app.get('*.js', async (req, res) => {
        try {
            const scriptPath = path.join(tenantRoot, req.path);

            // 检查文件是否存在
            if (!fs.existsSync(scriptPath)) {
                return res.status(404).send('Not Found');
            }

            // 读取并执行脚本
            const code = fs.readFileSync(scriptPath, 'utf8');

            // 在沙箱中执行
            const result = await sandbox.execute(code, req.path);

            res.send(result);
        } catch (error) {
            res.status(500).send(`Error: ${error.message}`);
        }
    });

    // 启动服务器
    const port = process.env.PORT || 3000;
    app.listen(port, () => {
        console.log(`Tenant ${tenantId} worker listening on port ${port}`);
    });
}

4. 使用Docker实现更强的隔离

// docker-sandbox.js
const Docker = require('dockerode');
const docker = new Docker();

class DockerSandbox {
    constructor(tenantConfig) {
        this.tenantId = tenantConfig.id;
        this.config = tenantConfig;
        this.container = null;
    }

    async createContainer() {
        // 创建Docker容器
        this.container = await docker.createContainer({
            Image: 'node:14-alpine',
            name: `tenant-${this.tenantId}`,
            Hostname: `tenant-${this.tenantId}`,

            // 资源限制
            HostConfig: {
                Memory: this.config.maxMemory,
                CpuShares: Math.floor(this.config.maxCpu * 1024),

                // 绑定挂载租户目录
                Binds: [
                    `${this.config.rootDir}:/app:rw`
                ],

                // 网络隔离
                NetworkMode: 'bridge',

                // 只读根文件系统
                ReadonlyRootfs: false,

                // 安全选项
                SecurityOpt: ['no-new-privileges'],
                CapDrop: ['ALL'],
                CapAdd: ['NET_BIND_SERVICE']
            },

            // 环境变量
            Env: [
                `TENANT_ID=${this.tenantId}`,
                `NODE_ENV=production`
            ],

            // 工作目录
            WorkingDir: '/app',

            // 用户
            User: `${this.config.uid}:${this.config.gid}`,

            // 命令
            Cmd: ['node', '/app/index.js']
        });

        await this.container.start();
    }

    async executeCode(code) {
        // 在容器中执行代码
        const exec = await this.container.exec({
            Cmd: ['node', '-e', code],
            AttachStdout: true,
            AttachStderr: true,
            WorkingDir: '/app'
        });

        const stream = await exec.start();

        return new Promise((resolve, reject) => {
            let output = '';

            stream.on('data', (chunk) => {
                output += chunk.toString();
            });

            stream.on('end', () => {
                resolve(output);
            });

            stream.on('error', reject);
        });
    }

    async stop() {
        if (this.container) {
            await this.container.stop();
            await this.container.remove();
        }
    }
}

5. 路由分发器

// dispatcher.js
const httpProxy = require('http-proxy');
const proxy = httpProxy.createProxyServer({});

class RequestDispatcher {
    constructor(virtualHostManager) {
        this.vhm = virtualHostManager;
    }

    createServer() {
        return http.createServer((req, res) => {
            // 根据域名获取租户
            const host = req.headers.host;
            const tenant = this.findTenantByHost(host);

            if (!tenant) {
                res.writeHead(404);
                res.end('Tenant not found');
                return;
            }

            // 检查并启动租户工作进程
            if (!this.vhm.workers.has(tenant.id)) {
                this.vhm.spawnTenantWorker(tenant.id);
            }

            // 代理请求到租户工作进程
            proxy.web(req, res, {
                target: `http://localhost:${tenant.port}`
            });
        });
    }

    findTenantByHost(host) {
        for (const [id, tenant] of this.vhm.tenants) {
            if (tenant.domain === host) {
                return tenant;
            }
        }
        return null;
    }
}

6. 配置示例

// config.js
module.exports = {
    tenants: [
        {
            id: 'tenant1',
            domain: 'tenant1.example.com',
            rootDir: '/var/www/tenants/tenant1',
            uid: 1001,
            gid: 1001,
            maxMemory: 134217728, // 128MB
            maxCpu: 0.5,
            port: 3001
        },
        {
            id: 'tenant2',
            domain: 'tenant2.example.com',
            rootDir: '/var/www/tenants/tenant2',
            uid: 1002,
            gid: 1002,
            maxMemory: 268435456, // 256MB
            maxCpu: 1.0,
            port: 3002
        }
    ]
};

7. 主程序

// main.js
const VirtualHostManager = require('./master');
const RequestDispatcher = require('./dispatcher');
const config = require('./config');

const vhm = new VirtualHostManager();

// 注册所有租户
config.tenants.forEach(tenant => {
    vhm.registerTenant(tenant.id, tenant);
    vhm.spawnTenantWorker(tenant.id);
});

// 创建分发服务器
const dispatcher = new RequestDispatcher(vhm);
const server = dispatcher.createServer();

server.listen(80, () => {
    console.log('Multi-tenant server running on port 80');
});

这个方案提供了:

进程隔离:每个租户运行在独立的进程中

文件系统隔离:限制访问范围在租户目录内

资源限制:CPU和内存限制

安全沙箱:VM模块提供代码执行隔离

Docker支持:可选的容器级隔离

网友回复

我知道答案,我要回答