feat: add ssh server (sshd)
This commit is contained in:
1 parent
69725658cf
commit
0a91131cb0
15 files changed
+2365
-4
No files matched your search
@@ -0,0 +1,29 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
|
||||
-ffreestanding -fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
|
||||
-ffunction-sections -fdata-sections -msse -msse2 -MMD -MP -I $(PROG_INC) \
|
||||
-I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
TARGET := $(BINDIR)/apps/sshserver/sshserver.elf
|
||||
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
-include $(OBJS:.o=.d)
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS SSH Server settings applet
|
||||
*/
|
||||
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/ssh.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 500;
|
||||
static constexpr int PAD = 20;
|
||||
static constexpr int FOOTER_H = 48;
|
||||
static constexpr int USER_ROW_H = 38;
|
||||
static constexpr int VISIBLE_USERS = 7;
|
||||
|
||||
static WsWindow g_win;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static bool g_is_admin = false;
|
||||
static bool g_enabled = false;
|
||||
static bool g_saved_enabled = false;
|
||||
static bool g_dirty = false;
|
||||
static montauk::user::UserInfo g_users[montauk::user::MAX_USERS];
|
||||
static bool g_allowed[montauk::user::MAX_USERS];
|
||||
static bool g_saved_allowed[montauk::user::MAX_USERS];
|
||||
static int g_user_count = 0;
|
||||
static int g_user_scroll = 0;
|
||||
static char g_status[128] = {};
|
||||
|
||||
static Rect service_row() {
|
||||
return {PAD, 76, WIN_W - PAD * 2, 32};
|
||||
}
|
||||
|
||||
static Rect user_row(int visibleIndex) {
|
||||
return {PAD, 164 + visibleIndex * USER_ROW_H,
|
||||
WIN_W - PAD * 2, USER_ROW_H};
|
||||
}
|
||||
|
||||
static Rect apply_button() {
|
||||
return {WIN_W - PAD - 92, WIN_H - FOOTER_H + 8, 92, 31};
|
||||
}
|
||||
|
||||
static Rect revert_button() {
|
||||
Rect apply = apply_button();
|
||||
return {apply.x - 104, apply.y, 92, apply.h};
|
||||
}
|
||||
|
||||
static mtk::Theme app_theme() {
|
||||
return mtk::make_theme(g_accent);
|
||||
}
|
||||
|
||||
static void load_state() {
|
||||
char currentUser[32] = {};
|
||||
montauk::getuser(currentUser, sizeof(currentUser));
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
g_enabled = init.get_bool("services.ssh.enabled", false);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
g_user_count = montauk::user::load_users(
|
||||
g_users, montauk::user::MAX_USERS);
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_allowed[i] = montauk::ssh::user_allowed(ssh, g_users[i].username);
|
||||
}
|
||||
ssh.destroy();
|
||||
|
||||
/* Reuse the table just loaded rather than parsing the user file twice. */
|
||||
g_is_admin = false;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (montauk::streq(g_users[i].username, currentUser)) {
|
||||
g_is_admin = montauk::streq(g_users[i].role, "admin");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_status[0] = 0;
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
}
|
||||
|
||||
static void recompute_dirty() {
|
||||
g_dirty = g_enabled != g_saved_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (g_allowed[i] != g_saved_allowed[i]) g_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool save_state() {
|
||||
if (!g_is_admin) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Administrator access is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
/*
|
||||
* init discovers services by enumerating [services.<id>] tables and needs
|
||||
* a path to launch. On a system whose init.toml predates the SSH service,
|
||||
* writing only the enabled flag would produce a key init cannot act on, so
|
||||
* write the whole service definition and let set_* overwrite in place where
|
||||
* it already exists.
|
||||
*/
|
||||
if (!init.get_string("services.ssh.path", nullptr)) {
|
||||
montauk::config::set_string(&init, "services.ssh.path",
|
||||
"0:/os/sshd.elf");
|
||||
montauk::config::set_string(&init, "services.ssh.name", "SSH server");
|
||||
montauk::config::set_bool(&init, "services.ssh.wait", false);
|
||||
montauk::config::set_bool(&init, "services.ssh.optional", true);
|
||||
}
|
||||
montauk::config::set_bool(&init, "services.ssh.enabled", g_enabled);
|
||||
int initResult = montauk::config::save("init", &init);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
char key[64];
|
||||
montauk::ssh::allow_key(key, sizeof(key), g_users[i].username);
|
||||
montauk::config::set_bool(&ssh, key, g_allowed[i]);
|
||||
}
|
||||
int sshResult = montauk::config::save("ssh", &ssh);
|
||||
ssh.destroy();
|
||||
|
||||
if (initResult < 0 || sshResult < 0) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Could not save SSH configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
g_enabled ? "Saved; SSH starts on the next boot"
|
||||
: "Saved; SSH is disabled on the next boot");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas canvas = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
canvas.fill(theme.window_bg);
|
||||
|
||||
canvas.text(PAD, 20, "SSH Server", theme.text);
|
||||
canvas.text(PAD, 44,
|
||||
"Secure remote access to the MontaukOS shell",
|
||||
theme.text_subtle);
|
||||
|
||||
Rect service = service_row();
|
||||
mtk::draw_checkbox(canvas, service,
|
||||
"Start the SSH server at boot",
|
||||
mtk::check_state(g_enabled), theme,
|
||||
g_is_admin,
|
||||
service.contains(g_mouse_x, g_mouse_y));
|
||||
canvas.text(PAD + 24, 112,
|
||||
"Listens on TCP port 22 and uses MontaukOS account passwords.",
|
||||
theme.text_subtle);
|
||||
canvas.text(PAD, 142, "USERS ALLOWED TO CONNECT", theme.text_muted);
|
||||
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex >= g_user_count) break;
|
||||
|
||||
Rect bounds = user_row(row);
|
||||
if (userIndex & 1) {
|
||||
canvas.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 55));
|
||||
}
|
||||
|
||||
char label[112];
|
||||
const char* displayName = g_users[userIndex].display_name[0]
|
||||
? g_users[userIndex].display_name
|
||||
: g_users[userIndex].username;
|
||||
snprintf(label, sizeof(label), "%s (%s)",
|
||||
displayName, g_users[userIndex].username);
|
||||
mtk::draw_checkbox(canvas, bounds, label,
|
||||
mtk::check_state(g_allowed[userIndex]),
|
||||
theme, g_is_admin,
|
||||
bounds.contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
if (montauk::streq(g_users[userIndex].role, "admin")) {
|
||||
const char* role = "Administrator";
|
||||
canvas.text(bounds.x + bounds.w - text_width(role) - 10,
|
||||
bounds.y + 10, role, theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
/* Without an indicator there is nothing to show the list continues. */
|
||||
if (g_user_count > VISIBLE_USERS) {
|
||||
Rect track = {WIN_W - PAD + 4, user_row(0).y, 4,
|
||||
VISIBLE_USERS * USER_ROW_H};
|
||||
canvas.fill_rect(track.x, track.y, track.w, track.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 120));
|
||||
|
||||
int thumbHeight = track.h * VISIBLE_USERS / g_user_count;
|
||||
if (thumbHeight < 20) thumbHeight = 20;
|
||||
int span = g_user_count - VISIBLE_USERS;
|
||||
int thumbY = track.y +
|
||||
(track.h - thumbHeight) * g_user_scroll / (span ? span : 1);
|
||||
canvas.fill_rect(track.x, thumbY, track.w, thumbHeight,
|
||||
theme.text_muted);
|
||||
}
|
||||
|
||||
if (!g_is_admin) {
|
||||
canvas.text(PAD, WIN_H - FOOTER_H - 27,
|
||||
"Only administrators can change system SSH settings.",
|
||||
theme.danger);
|
||||
}
|
||||
|
||||
Rect footer = {0, WIN_H - FOOTER_H, WIN_W, FOOTER_H};
|
||||
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
|
||||
mtk::draw_separator(canvas, 0, footer.y, WIN_W, theme);
|
||||
const char* footerStatus = g_status[0]
|
||||
? g_status
|
||||
: (g_dirty ? "Unsaved SSH changes" : "SSH configuration ready");
|
||||
canvas.text(PAD, footer.y + 16, footerStatus, theme.text_subtle);
|
||||
|
||||
Rect revert = revert_button();
|
||||
Rect apply = apply_button();
|
||||
mtk::draw_button(
|
||||
canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, revert.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
mtk::draw_button(
|
||||
canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, apply.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
|
||||
host.present();
|
||||
}
|
||||
|
||||
static void handle_mouse(const montauk::abi::WinEvent& event) {
|
||||
g_mouse_x = event.mouse.x;
|
||||
g_mouse_y = event.mouse.y;
|
||||
|
||||
bool pressed = (event.mouse.buttons & 1) &&
|
||||
!(event.mouse.prev_buttons & 1);
|
||||
|
||||
if (event.mouse.scroll) {
|
||||
g_user_scroll += event.mouse.scroll > 0 ? -1 : 1;
|
||||
int maximum = g_user_count > VISIBLE_USERS
|
||||
? g_user_count - VISIBLE_USERS
|
||||
: 0;
|
||||
if (g_user_scroll < 0) g_user_scroll = 0;
|
||||
if (g_user_scroll > maximum) g_user_scroll = maximum;
|
||||
}
|
||||
|
||||
if (!pressed || !g_is_admin) return;
|
||||
|
||||
if (service_row().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_enabled = !g_enabled;
|
||||
/* A stale "Saved" message would otherwise hide the unsaved state. */
|
||||
g_status[0] = 0;
|
||||
} else {
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex < g_user_count &&
|
||||
user_row(row).contains(g_mouse_x, g_mouse_y)) {
|
||||
g_allowed[userIndex] = !g_allowed[userIndex];
|
||||
g_status[0] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revert_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
load_state();
|
||||
} else if (apply_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
save_state();
|
||||
}
|
||||
recompute_dirty();
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
|
||||
g_accent = mtk::load_system_accent();
|
||||
load_state();
|
||||
if (!g_win.create("SSH Server", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent event;
|
||||
int count = g_win.poll(&event);
|
||||
if (count <= 0) {
|
||||
montauk::yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == 1) {
|
||||
handle_mouse(event);
|
||||
} else if (event.type == 2) {
|
||||
g_win.width = event.resize.w;
|
||||
g_win.height = event.resize.h;
|
||||
} else if (event.type == 3) {
|
||||
g_win.closed = true;
|
||||
} else if (event.type == 0 && event.key.pressed &&
|
||||
event.key.scancode == 1) {
|
||||
g_win.closed = true;
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[app]
|
||||
id = "sshserver"
|
||||
name = "SSH Server"
|
||||
binary = "sshserver.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
|
||||
[desktop]
|
||||
section = "settings"
|
||||
admin_only = false
|
||||
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in new issue
Block a user