feat: add ssh server (sshd)

This commit is contained in:
2026-08-11 12:53:36 +02:00
parent 69725658cf
commit 0a91131cb0
15 changed files with 2365 additions and 4 deletions
+26
View File
@@ -0,0 +1,26 @@
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
CXX := $(TOOLCHAIN_PREFIX)g++
PROG_INC := ../../include
LIBDIR := ../../lib
LINK_LD := ../../link.ld
OBJDIR := obj
TARGET := ../../bin/os/sshd.elf
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -ffreestanding \
-fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
-ffunction-sections -fdata-sections -mno-80387 -mno-mmx -mno-sse -mno-sse2 \
-MMD -MP -I $(PROG_INC) -I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJDIR)/main.o $(LIBS) $(LINK_LD) Makefile
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJDIR)/main.o $(LIBS) -o $@
$(OBJDIR)/main.o: main.cpp crypto.hpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c main.cpp -o $@
-include $(OBJDIR)/main.d
clean:
rm -rf $(OBJDIR) $(TARGET)
+365
View File
@@ -0,0 +1,365 @@
#pragma once
#include <montauk/config.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
#include <montauk/user.h>
#include <bearssl_block.h>
#include <bearssl_hash.h>
#include <bearssl_hmac.h>
#include <bearssl_rand.h>
#include <bearssl_rsa.h>
namespace sshcrypto {
static inline uint32_t be32(const uint8_t* data) {
return ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
data[3];
}
static inline void put32(uint8_t* data, uint32_t value) {
data[0] = value >> 24;
data[1] = value >> 16;
data[2] = value >> 8;
data[3] = value;
}
/*
* Wipe key material. Written through a volatile pointer so the optimiser
* cannot drop the store as dead when the buffer dies with the frame.
*/
static inline void zeroize(void* data, int size) {
volatile uint8_t* p = (volatile uint8_t*)data;
for (int i = 0; i < size; i++) p[i] = 0;
}
static inline void sha256(const void* data, int size, uint8_t output[32]) {
br_sha256_context context;
br_sha256_init(&context);
br_sha256_update(&context, data, size);
br_sha256_out(&context, output);
}
struct Rng {
br_hmac_drbg_context context;
bool init() {
uint8_t seed[64];
int64_t received = montauk::getrandom(seed, sizeof(seed));
if (received != (int64_t)sizeof(seed)) {
montauk::memset(seed, 0, sizeof(seed));
return false;
}
br_hmac_drbg_init(&context, &br_sha256_vtable, seed, sizeof(seed));
montauk::memset(seed, 0, sizeof(seed));
return true;
}
void bytes(void* output, int size) {
br_hmac_drbg_generate(&context, output, size);
}
};
static inline void hex(const uint8_t* input, int size, char* output) {
static const char* digits = "0123456789abcdef";
for (int i = 0; i < size; i++) {
output[i * 2] = digits[input[i] >> 4];
output[i * 2 + 1] = digits[input[i] & 15];
}
output[size * 2] = 0;
}
static inline int unhex(const char* input, uint8_t* output, int capacity) {
int size = 0;
while (input && input[0] && input[1] && size < capacity) {
int high = montauk::user::hex_char_val(input[0]);
int low = montauk::user::hex_char_val(input[1]);
if (high < 0 || low < 0) break;
output[size++] = (high << 4) | low;
input += 2;
}
return size;
}
/*
* The host key lives in its own config file rather than in ssh.toml so that
* the unprivileged Settings applet, which rewrites ssh.toml whenever the
* per-user allow table changes, never reads or round-trips private key
* material. Note this is separation, not protection: the filesystem has no
* permission model yet, so any local process can still read the file. Real
* confidentiality needs FS-level access control.
*/
static const char HOSTKEY_CONFIG[] = "ssh_hostkey";
static const char HOSTKEY_LEGACY_CONFIG[] = "ssh";
struct HostKey {
uint8_t privateBuffer[BR_RSA_KBUF_PRIV_SIZE(2048)];
uint8_t publicBuffer[BR_RSA_KBUF_PUB_SIZE(2048)];
br_rsa_private_key sk = {};
br_rsa_public_key pk = {};
/*
* Earlier builds kept the key in ssh.toml. Move any such key into its own
* file on first start so an upgrade does not change the host key (which
* would trip every client's known-hosts check).
*/
bool load() {
if (load_from(HOSTKEY_CONFIG)) return true;
if (!load_from(HOSTKEY_LEGACY_CONFIG)) return false;
migrate_from_legacy();
return true;
}
bool load_from(const char* configName) {
auto config = montauk::config::load(configName);
const char* modulus = config.get_string("host_key.n", "");
const char* exponent = config.get_string("host_key.e", "");
const char* primeP = config.get_string("host_key.p", "");
const char* primeQ = config.get_string("host_key.q", "");
const char* exponentP = config.get_string("host_key.dp", "");
const char* exponentQ = config.get_string("host_key.dq", "");
const char* coefficient = config.get_string("host_key.iq", "");
int offset = 0;
pk.n = publicBuffer;
pk.nlen = unhex(modulus, publicBuffer, sizeof(publicBuffer));
pk.e = publicBuffer + pk.nlen;
pk.elen = unhex(exponent, publicBuffer + pk.nlen,
sizeof(publicBuffer) - pk.nlen);
sk.p = privateBuffer + offset;
sk.plen = unhex(primeP, sk.p, sizeof(privateBuffer) - offset);
offset += sk.plen;
sk.q = privateBuffer + offset;
sk.qlen = unhex(primeQ, sk.q, sizeof(privateBuffer) - offset);
offset += sk.qlen;
sk.dp = privateBuffer + offset;
sk.dplen = unhex(exponentP, sk.dp, sizeof(privateBuffer) - offset);
offset += sk.dplen;
sk.dq = privateBuffer + offset;
sk.dqlen = unhex(exponentQ, sk.dq, sizeof(privateBuffer) - offset);
offset += sk.dqlen;
sk.iq = privateBuffer + offset;
sk.iqlen = unhex(coefficient, sk.iq, sizeof(privateBuffer) - offset);
sk.n_bitlen = (uint32_t)config.get_int("host_key.bits", 2048);
config.destroy();
return pk.nlen >= 256 && pk.elen > 0 &&
sk.plen > 0 && sk.qlen > 0 &&
sk.dplen > 0 && sk.dqlen > 0 && sk.iqlen > 0;
}
/* Rewrite the in-memory key to its own file and strip it from ssh.toml. */
void migrate_from_legacy() {
if (save_to(HOSTKEY_CONFIG) < 0) return;
auto legacy = montauk::config::load(HOSTKEY_LEGACY_CONFIG);
static const char* const parts[] = {
"host_key.n", "host_key.e", "host_key.p", "host_key.q",
"host_key.dp", "host_key.dq", "host_key.iq", "host_key.bits",
};
for (unsigned i = 0; i < sizeof(parts) / sizeof(parts[0]); i++) {
montauk::config::unset(&legacy, parts[i]);
}
montauk::config::save(HOSTKEY_LEGACY_CONFIG, &legacy);
legacy.destroy();
}
int save_to(const char* configName) {
auto config = montauk::config::load(configName);
char encoded[520];
#define SAVE_PART(key, data, size) \
do { \
hex((const uint8_t*)(data), (int)(size), encoded); \
montauk::config::set_string(&config, "host_key." key, encoded); \
} while (0)
SAVE_PART("n", pk.n, pk.nlen);
SAVE_PART("e", pk.e, pk.elen);
SAVE_PART("p", sk.p, sk.plen);
SAVE_PART("q", sk.q, sk.qlen);
SAVE_PART("dp", sk.dp, sk.dplen);
SAVE_PART("dq", sk.dq, sk.dqlen);
SAVE_PART("iq", sk.iq, sk.iqlen);
#undef SAVE_PART
montauk::config::set_int(&config, "host_key.bits", sk.n_bitlen);
int result = montauk::config::save(configName, &config);
config.destroy();
montauk::memset(encoded, 0, sizeof(encoded));
return result;
}
bool generate(Rng& rng) {
if (!br_rsa_i31_keygen((const br_prng_class**)&rng.context,
&sk, privateBuffer,
&pk, publicBuffer,
2048, 65537)) {
return false;
}
return save_to(HOSTKEY_CONFIG) >= 0;
}
};
/* RFC 3526 MODP group 14, represented as little-endian 32-bit limbs. */
static constexpr int NL = 64;
static const uint32_t P[NL] = {
0xFFFFFFFF, 0xFFFFFFFF, 0x8AACAA68, 0x15728E5A,
0x98FA0510, 0x15D22618, 0xEA956AE5, 0x3995497C,
0x95581718, 0xDE2BCBF6, 0x6F4C52C9, 0xB5C55DF0,
0xEC07A28F, 0x9B2783A2, 0x180E8603, 0xE39E772C,
0x2E36CE3B, 0x32905E46, 0xCA18217C, 0xF1746C08,
0x4ABC9804, 0x670C354E, 0x7096966D, 0x9ED52907,
0x208552BB, 0x1C62F356, 0xDCA3AD96, 0x83655D23,
0xFD24CF5F, 0x69163FA8, 0x1C55D39A, 0x98DA4836,
0xA163BF05, 0xC2007CB8, 0xECE45B3D, 0x49286651,
0x7C4B1FE6, 0xAE9F2411, 0x5A899FA5, 0xEE386BFB,
0xF406B7ED, 0x0BFF5CB6, 0xA637ED6B, 0xF44C42E9,
0x625E7EC6, 0xE485B576, 0x6D51C245, 0x4FE1356D,
0xF25F1437, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3,
0x8E3404DD, 0x514A0879, 0x3B139B22, 0x020BBEA6,
0x8A67CC74, 0x29024E08, 0x80DC1CD1, 0xC4C6628B,
0x2168C234, 0xC90FDAA2, 0xFFFFFFFF, 0xFFFFFFFF,
};
static inline int cmp(const uint32_t* left, const uint32_t* right) {
for (int i = NL - 1; i >= 0; i--) {
if (left[i] != right[i]) return left[i] > right[i] ? 1 : -1;
}
return 0;
}
static inline void subp(uint32_t* value) {
uint64_t borrow = 0;
for (int i = 0; i < NL; i++) {
uint64_t difference = (uint64_t)value[i] - P[i] - borrow;
value[i] = (uint32_t)difference;
borrow = (difference >> 63) & 1;
}
}
static inline void dbl(uint32_t* value) {
uint64_t carry = 0;
for (int i = 0; i < NL; i++) {
uint64_t doubled = ((uint64_t)value[i] << 1) | carry;
value[i] = doubled;
carry = doubled >> 32;
}
if (carry || cmp(value, P) >= 0) subp(value);
}
static inline void montmul(const uint32_t* left, const uint32_t* right,
uint32_t* output) {
uint32_t temporary[NL + 1] = {};
/* P[0] is -1, therefore -P^-1 mod 2^32 is 1. */
for (int i = 0; i < NL; i++) {
uint64_t carry = 0;
for (int j = 0; j < NL; j++) {
uint64_t product = (uint64_t)left[j] * right[i] +
temporary[j] + carry;
temporary[j] = product;
carry = product >> 32;
}
uint64_t sum = (uint64_t)temporary[NL] + carry;
temporary[NL] = (uint32_t)sum;
uint32_t multiplier = temporary[0];
carry = 0;
for (int j = 0; j < NL; j++) {
sum = (uint64_t)multiplier * P[j] + temporary[j] + carry;
if (j > 0) temporary[j - 1] = (uint32_t)sum;
carry = sum >> 32;
}
sum = (uint64_t)temporary[NL] + carry;
temporary[NL - 1] = (uint32_t)sum;
temporary[NL] = (uint32_t)(sum >> 32);
}
for (int i = 0; i < NL; i++) output[i] = temporary[i];
if (temporary[NL] || cmp(output, P) >= 0) subp(output);
}
static inline void from_be(const uint8_t* input, int size, uint32_t* output) {
montauk::memset(output, 0, NL * 4);
for (int i = 0; i < size && i < 256; i++) {
output[i / 4] |= (uint32_t)input[size - 1 - i] << ((i & 3) * 8);
}
}
static inline void to_be(const uint32_t* input, uint8_t output[256]) {
for (int i = 0; i < 256; i++) {
output[255 - i] = (uint8_t)(input[i / 4] >> ((i & 3) * 8));
}
}
static inline void modexp(const uint32_t* base, const uint8_t* exponent,
int exponentSize, uint32_t* output) {
uint32_t one[NL] = {};
uint32_t rSquared[NL];
uint32_t result[NL];
uint32_t montgomeryBase[NL];
uint32_t temporary[NL];
one[0] = 1;
for (int i = 0; i < NL * 32; i++) dbl(one);
for (int i = 0; i < NL; i++) rSquared[i] = one[i];
for (int i = 0; i < NL * 32; i++) dbl(rSquared);
for (int i = 0; i < NL; i++) result[i] = one[i];
montmul(base, rSquared, montgomeryBase);
for (int i = 0; i < exponentSize; i++) {
for (int bit = 7; bit >= 0; bit--) {
montmul(result, result, temporary);
for (int j = 0; j < NL; j++) result[j] = temporary[j];
if ((exponent[i] >> bit) & 1) {
montmul(result, montgomeryBase, temporary);
for (int j = 0; j < NL; j++) result[j] = temporary[j];
}
}
}
uint32_t normal[NL] = {};
normal[0] = 1;
montmul(result, normal, output);
}
struct Cipher {
br_aes_ct_ctr_keys aes;
uint8_t iv[12];
uint32_t counter = 0;
uint8_t mac[32];
void init(const uint8_t* key, const uint8_t* initializationVector,
const uint8_t* macKey) {
br_aes_ct_ctr_init(&aes, key, 16);
montauk::memcpy(iv, initializationVector, sizeof(iv));
counter = be32(initializationVector + 12);
montauk::memcpy(mac, macKey, sizeof(mac));
}
void crypt(void* data, int size) {
counter = br_aes_ct_ctr_run(&aes, iv, counter, data, size);
}
};
static inline void hmac(const uint8_t key[32], const void* data, int size,
uint8_t output[32]) {
br_hmac_key_context keyContext;
br_hmac_context context;
br_hmac_key_init(&keyContext, &br_sha256_vtable, key, 32);
br_hmac_init(&context, &keyContext, 32);
br_hmac_update(&context, data, size);
br_hmac_out(&context, output);
}
} // namespace sshcrypto
File diff suppressed because it is too large Load Diff
+29
View File
@@ -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)
+328
View File
@@ -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);
}
+13
View File
@@ -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>