|
|
|
@@ -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);
|
|
|
|
|
}
|