feat: initial x3-ssh client

- GTK3 GUI with VTE terminal emulation
- Tabbed SSH sessions
- Server management (add/edit/remove)
- SSH key management (generate/import/delete)
- Quick connect bar
- libssh for SSH connections
- autotools build system (configure && make)
This commit is contained in:
x3
2026-09-07 14:50:20 +00:00
commit 77f9568cb1
15 changed files with 1476 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
#include "key_store.h"
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <libssh/libssh.h>
static std::string trim(const std::string& s) {
size_t a = s.find_first_not_of(" \t\r\n");
if (a == std::string::npos) return "";
size_t b = s.find_last_not_of(" \t\r\n");
return s.substr(a, b - a + 1);
}
static std::string get_fingerprint(const std::string& pubkey_path) {
std::string cmd = "ssh-keygen -lf \"" + pubkey_path + "\" 2>/dev/null";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return "";
char buf[512];
std::string result;
if (fgets(buf, sizeof(buf), pipe)) {
result = trim(buf);
/* Remove the key type prefix like "(RSA)" */
auto paren = result.find('(');
if (paren != std::string::npos) {
result = trim(result.substr(0, paren));
}
}
pclose(pipe);
return result;
}
KeyStore::KeyStore() {
const char* home = getenv("HOME");
if (!home) home = ".";
config_path_ = std::string(home) + "/.config/sshclient/keys.conf";
keys_dir_ = std::string(home) + "/.config/sshclient/keys";
std::string cmd = "mkdir -p \"" + keys_dir_ + "\"";
system(cmd.c_str());
load();
}
void KeyStore::load() {
keys_.clear();
std::ifstream f(config_path_);
if (!f.is_open()) return;
SshKey current;
bool in_key = false;
std::string line;
while (std::getline(f, line)) {
line = trim(line);
if (line.empty() || line[0] == '#') {
if (in_key && !current.name.empty()) {
keys_.push_back(current);
current = SshKey();
in_key = false;
}
continue;
}
if (line[0] == '[' && line.back() == ']') {
if (in_key && !current.name.empty()) {
keys_.push_back(current);
current = SshKey();
}
current.name = trim(line.substr(1, line.size() - 2));
in_key = true;
continue;
}
if (!in_key) continue;
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = trim(line.substr(0, eq));
std::string val = trim(line.substr(eq + 1));
if (key == "pubkey_path") current.pubkey_path = val;
else if (key == "privkey_path") current.privkey_path = val;
else if (key == "fingerprint") current.fingerprint = val;
}
if (in_key && !current.name.empty()) {
keys_.push_back(current);
}
}
void KeyStore::save() const {
std::ofstream f(config_path_);
if (!f.is_open()) return;
f << "# SSH Client - Stored Keys\n\n";
for (const auto& k : keys_) {
f << "[" << k.name << "]\n";
f << "pubkey_path=" << k.pubkey_path << "\n";
f << "privkey_path=" << k.privkey_path << "\n";
f << "fingerprint=" << k.fingerprint << "\n\n";
}
}
void KeyStore::add(const SshKey& k) {
keys_.push_back(k);
save();
}
void KeyStore::remove(int index) {
if (index >= 0 && index < (int)keys_.size()) {
keys_.erase(keys_.begin() + index);
save();
}
}
bool KeyStore::generate_keypair(const std::string& name, int bits) {
std::string priv_path = keys_dir_ + "/" + name;
std::string pub_path = priv_path + ".pub";
std::string cmd = "ssh-keygen -t rsa -b " + std::to_string(bits) +
" -f \"" + priv_path + "\" -N \"\" -C \"" + name + "\" 2>/dev/null";
int rc = system(cmd.c_str());
if (rc != 0) return false;
SshKey k;
k.name = name;
k.privkey_path = priv_path;
k.pubkey_path = pub_path;
k.fingerprint = get_fingerprint(pub_path);
add(k);
return true;
}
bool KeyStore::import_key(const std::string& name, const std::string& privkey_path) {
ssh_key key = nullptr;
if (ssh_pki_import_privkey_file(privkey_path.c_str(), nullptr, nullptr, nullptr, &key) != SSH_OK) {
return false;
}
ssh_key_free(key);
std::string dest = keys_dir_ + "/" + name;
std::string cp_cmd = "cp \"" + privkey_path + "\" \"" + dest + "\" && chmod 600 \"" + dest + "\"";
int rc = system(cp_cmd.c_str());
if (rc != 0) return false;
std::string pub_src = privkey_path + ".pub";
std::string pub_dest = dest + ".pub";
std::string cp_pub = "cp \"" + pub_src + "\" \"" + pub_dest + "\" 2>/dev/null";
system(cp_pub.c_str());
SshKey k;
k.name = name;
k.privkey_path = dest;
k.pubkey_path = pub_dest;
k.fingerprint = get_fingerprint(pub_dest);
add(k);
return true;
}
SshKey KeyStore::find_by_name(const std::string& name) const {
for (const auto& k : keys_) {
if (k.name == name) return k;
}
return {};
}