From 2993b887bd4681cd3c1e2f980c0ab499f3216239 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 10 Jan 2025 14:52:23 -0500 Subject: [PATCH] dev: experimental local-terminal service --- src/backend/exports.js | 2 + src/backend/src/data/hardcoded-permissions.js | 1 + .../modules/development/DevelopmentModule.js | 39 ++++ .../development/LocalTerminalService.js | 173 ++++++++++++++++++ .../src/modules/web/WebServerService.js | 1 + 5 files changed, 216 insertions(+) create mode 100644 src/backend/src/modules/development/DevelopmentModule.js create mode 100644 src/backend/src/modules/development/LocalTerminalService.js diff --git a/src/backend/exports.js b/src/backend/exports.js index 46cef0e8..2ff8500d 100644 --- a/src/backend/exports.js +++ b/src/backend/exports.js @@ -33,6 +33,7 @@ const { TemplateModule } = require("./src/modules/template/TemplateModule.js"); const { PuterFSModule } = require("./src/modules/puterfs/PuterFSModule.js"); const { PerfMonModule } = require("./src/modules/perfmon/PerfMonModule.js"); const { AppsModule } = require("./src/modules/apps/AppsModule.js"); +const { DevelopmentModule } = require("./src/modules/development/DevelopmentModule.js"); module.exports = { @@ -69,4 +70,5 @@ module.exports = { // Development modules PerfMonModule, + DevelopmentModule, }; diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js index 2dbfe0a1..d75685c3 100644 --- a/src/backend/src/data/hardcoded-permissions.js +++ b/src/backend/src/data/hardcoded-permissions.js @@ -97,6 +97,7 @@ const hardcoded_user_group_permissions = { 'service': {}, 'feature': {}, 'kernel-info': {}, + 'local-terminal:access': {}, }, 'b7220104-7905-4985-b996-649fdcdb3c8f': { 'service:hello-world:ii:hello-world': policy_perm('temp.es'), diff --git a/src/backend/src/modules/development/DevelopmentModule.js b/src/backend/src/modules/development/DevelopmentModule.js new file mode 100644 index 00000000..409f9dea --- /dev/null +++ b/src/backend/src/modules/development/DevelopmentModule.js @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { AdvancedBase } = require("@heyputer/putility"); + +/** + * Enable this module when you want performance monitoring. + * + * Performance monitoring requires additional setup. Jaegar should be installed + * and running. + */ +class DevelopmentModule extends AdvancedBase { + async install (context) { + const services = context.get('services'); + + const LocalTerminalService = require("./LocalTerminalService"); + services.registerService('local-terminal', LocalTerminalService); + } +} + +module.exports = { + DevelopmentModule, +}; diff --git a/src/backend/src/modules/development/LocalTerminalService.js b/src/backend/src/modules/development/LocalTerminalService.js new file mode 100644 index 00000000..3fc2016a --- /dev/null +++ b/src/backend/src/modules/development/LocalTerminalService.js @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { spawn } = require("child_process"); +const APIError = require("../../api/APIError"); +const configurable_auth = require("../../middleware/configurable_auth"); +const { Endpoint } = require("../../util/expressutil"); + + +const PERM_LOCAL_TERMINAL = 'local-terminal:access'; + +const path_ = require('path'); +const { Actor } = require("../../services/auth/Actor"); +const BaseService = require("../../services/BaseService"); +const { Context } = require("../../util/context"); + +class LocalTerminalService extends BaseService { + _construct () { + this.sessions_ = {}; + } + get_profiles () { + return { + ['api-test']: { + cwd: path_.join( + __dirname, + '../../../../../', + 'tools/api-tester', + ), + shell: ['/usr/bin/env', 'node', 'apitest.js'], + allow_args: true, + }, + }; + }; + ['__on_install.routes'] (_, { app }) { + const r_group = (() => { + const require = this.require; + const express = require('express'); + return express.Router() + })(); + app.use('/local-terminal', r_group); + + Endpoint({ + route: '/new', + methods: ['POST'], + mw: [configurable_auth()], + handler: async (req, res) => { + const term_uuid = require('uuid').v4(); + + const svc_permission = this.services.get('permission'); + const actor = Context.get('actor'); + const can_access = actor && + await svc_permission.check(actor, PERM_LOCAL_TERMINAL); + + if ( ! can_access ) { + throw APIError.create('permission_denied', null, { + permission: PERM_LOCAL_TERMINAL, + }); + } + + const profiles = this.get_profiles(); + if ( ! profiles[req.body.profile] ) { + throw APIError.create('invalid_profile', null, { + profile: req.body.profile, + }); + } + + const profile = profiles[req.body.profile]; + + const args = profile.shell.slice(1); + if ( ! profile.allow_args && req.body.args ) { + args.push(...req.body.args); + } + const proc = spawn(profile.shell[0], args, { + shell: true, + env: { + ...process.env, + ...(profile.env ?? {}), + }, + cwd: profile.cwd, + }); + + console.log('process??', proc); + + // stdout to websocket + { + const svc_socketio = req.services.get('socketio'); + proc.stdout.on('data', data => { + const base64 = data.toString('base64'); + console.log('---------------------- CHUNK?', base64); + svc_socketio.send( + { room: req.user.id }, + 'local-terminal.stdout', + { + term_uuid, + base64, + }, + ); + }); + proc.stderr.on('data', data => { + const base64 = data.toString('base64'); + console.log('---------------------- CHUNK?', base64); + svc_socketio.send( + { room: req.user.id }, + 'local-terminal.stderr', + { + term_uuid, + base64, + }, + ); + }); + } + + proc.on('exit', () => { + this.log.noticeme(`[${term_uuid}] Process exited (${proc.exitCode})`); + delete this.sessions_[term_uuid]; + }); + + this.sessions_[term_uuid] = { + uuid: term_uuid, + proc, + }; + + res.json({ term_uuid }); + }, + }).attach(r_group); + } + async _init () { + const svc_event = this.services.get('event'); + svc_event.on('web.socket.user-connected', async (_, { + socket, + user, + }) => { + const svc_permission = this.services.get('permission'); + const actor = Actor.adapt(user); + const can_access = actor && + await svc_permission.check(actor, PERM_LOCAL_TERMINAL); + + if ( ! can_access ) { + return; + } + + socket.on('local-terminal.stdin', async msg => { + console.log('local term message', msg); + + const session = this.sessions_[msg.term_uuid]; + if ( ! session ) { + return; + } + + const base64 = Buffer.from(msg.data, 'base64'); + session.proc.stdin.write(base64); + }) + }); + } +} + +module.exports = LocalTerminalService; diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js index ee6d10fe..3f910ba9 100644 --- a/src/backend/src/modules/web/WebServerService.js +++ b/src/backend/src/modules/web/WebServerService.js @@ -275,6 +275,7 @@ class WebServerService extends BaseService { actor: socket.actor, }).arun(async () => { await svc_event.emit('web.socket.user-connected', { + socket, user: socket.user }); });