Home labbing journal + an attempt to use Nginx JavaScript engine
Recently, I decided to give home labbing a try. I never needed it, but I found a few applications for such a hobby. This post serves as a journal of my learning/discoveries throughout the process. When you try something, you meet new problems, and writing is the best way to process them.
The main idea is to have a Telegram bot server (bot server) and a list of approved commands to external services hosted on the home lab server(the server). So it's possible to send a command from the bot server to an Nginx running on the server, which will route this command to the relevant application or execute some commands in the shell.
If possible, I would prefer not to run the bot server directly on the system, but rather in a Docker container. The same applies to every application on the homelab, except for Nginx and a few simple scripts used to collect information or run basic commands on the server. Why? IDK, it's more fun.
To get started, I want to add three commands
- Get an IP address of the system (because it's a Raspberry Pi, which I can move anywhere, and I don't want to connect it to a monitor or TV to learn its IP address to use VCN or SSH).
- Turn on/off the desktop environment, because why waste the juice of a not-so-powerful machine?
- Turn on/off Raspberry Pi Connect.
I expect that Nginx should allow for some basic scripting with JS, because JS is still on the rise and is everywhere, and it was possible to run PHP in the same way years ago, so it should be possible for Nginx now.
I got excited when I saw that there's an NJS engine built into Nginx, because I thought it would be much simpler just to have an Nginx server instead of creating a daemon Node.js application. Small atomic scripts in JS that can do something on the server and are accessible only by local requests from within the server. No build process, no maintenance, small proxy to the system shell.
And I got digging, and it looks pretty fun. You can actually perform some basic tasks with it (examples), such as reading/writing files, making separate requests, logging, balancing, etc.
In the specification of QuickJS I found that they have a wrapper of some sort around exec
.
I spent quite some time figuring out how to use it. I tried some approaches from GitHub, some examples from Node.js. But I could not find ANY examples of exec usage. I stopped once I found an issue in Nginx GH account: https://github.com/nginx/njs/issues/10. It's not possible to run shell commands... At all... It's counterintuitive because it's a feature that many people would love to have, and I can see the advantages of having it in Nginx instead of running a separate server behind Nginx.
Originally, I mistook QuickJS for NJS. Apparently, they're completely different species, but in different articles, they are mentioned interchangeably (like instead of NJS they use QuickJS and vice versa). I got excited again once I saw this issue https://github.com/nginx/njs/issues/698
They're going to implement QuickJS! And QuickJS must be able to run exec
on the host system! Probably...
The task is closed — NJS cannot run shell commands due to “design issues” mentioned in this issue. In the NJS process type definition, you can kill a process if you know its PID, but you can't create a new one.
interface NjsProcess {
readonly pid: number;
readonly ppid: number;
readonly argv: string[];
readonly env: NjsEnv;
/**
* @since 0.8.8
*/
kill(pid: number, signal?: string | number): true;
}
declare const process: NjsProcess;
With all my hopes, I went to dig into the repository again for details on QuickJS, and they have not implemented shell access for QuickJS yet (if they even intended to). At least I didn't find any possibility to do so. On the bright side, I haven't worked with C code in years, possibly since my university days. So it was fun.
Since it's not possible to make NJS and QuickJS in Nginx run shell commands, maybe there's another approach to it?
I'm a JS developer with .NET experience and even C++ experience. On the other hand, there's a limit to how much time I can spend on such projects. Usually, it's a rabbit hole where you fall deeper and deeper into a problem, and along the way you get even more problems around it.
This article suggests a few approaches. First of all, the usage of HttpLuaModule with an example of how to use it, Good old FastCGI, and one more way, which does not meet my needs.
As a result, I decided to skip the part of spending more time on Nginx setup and move directly to the bot server. For security reasons, I needed to limit access to the server, and the best way is a hardcoded check by account name.
By the way, I'm using grammy because it's faster to do it this way
import { Bot } from 'grammy';
import type { Context, NextFunction } from 'grammy';
const Token = process.env.TG_KEY || '';
const UserWhitelist = (process.env.USER_WHITELIST || '')
.split(',')
.map(x => x.trim());
const bot = new Bot(Token);
async function authSession(
ctx: Context,
next: NextFunction
): Promise<void> {
if (ctx.from?.username && !UserWhitelist.includes(ctx.from.username)) {
await ctx.reply('Sadly, you can not use the service');
return;
}
await next();
}
bot.use(authSession);
This way, I can be sure that no one will be able to connect to the home server without permission. I want the server to be a closed environment, except for the local network and the TG server. SSH will be enabled only for a short time by the bot, and all other operations will be permitted only from the bot as well.
and since it's a node.js server, we can easily run shell commands like
import * as util from 'util';
import * as chProcess from 'child_process';
const execAsync = util.promisify(chProcess.exec);
export const executeShell = async (command: string) => {
const { stdout, stderr } = await execAsync(command);
return [stdout, stderr]
}
The bot server is WIP, and I won't open the repo yet. Once it's done(and if it's done), I'll write a new post about it with more details on the internal information.
Thanks for reading 🎉