802 行
30 KiB
C
802 行
30 KiB
C
/* pssh ssh —— SSH2 客户端 (平替 ssh)
|
|
* 支持:交互 shell / 执行命令 / 端口转发 -L -R -D(SOCKS5) / 代理跳转 -J /
|
|
* config 文件 -F / ~/.ssh/config / ~/.pssh/config.conf / 公钥与 agent 认证
|
|
*/
|
|
#include "paze/ssh.h"
|
|
#include "paze/ssh_agent.h"
|
|
#include "paze/ssh_config.h"
|
|
#include "paze/ssh_tcpip.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <windows.h>
|
|
#include <conio.h>
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#define GETCH _getch
|
|
#define KBHIT _kbhit
|
|
#define STDIN_FD 0
|
|
#else
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#include <sys/select.h>
|
|
#include <sys/time.h>
|
|
static struct termios orig_tio;
|
|
static void raw_on(void) {
|
|
struct termios t;
|
|
tcgetattr(STDIN_FILENO, &orig_tio);
|
|
t = orig_tio;
|
|
t.c_lflag &= ~(ICANON | ECHO);
|
|
t.c_cc[VMIN] = 0;
|
|
t.c_cc[VTIME] = 0;
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &t);
|
|
}
|
|
static void raw_off(void) {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio);
|
|
}
|
|
#define STDIN_FD STDIN_FILENO
|
|
#endif
|
|
|
|
/* 转发规格 */
|
|
typedef struct {
|
|
int kind; /* SSH_FWD_L / SSH_FWD_R / SSH_FWD_D */
|
|
char bind[128];
|
|
int bind_port;
|
|
char target[256];
|
|
int target_port;
|
|
} fwd_spec_t;
|
|
|
|
#define PSSH_VERSION "Beta-0.0.1"
|
|
|
|
/* -v / -c 选项(跨 connect_target 传递) */
|
|
static int g_verbose = 0;
|
|
static const char *g_cipher = NULL;
|
|
/* -o ReconnectInterval=N:断线自动重连间隔秒(0=禁用)。断线时 pssh_cmd_ssh 返回 -2,
|
|
* 由 pssh main 循环重连。 */
|
|
static int g_reconnect = 0;
|
|
|
|
/* 供外层(pssh main)查询自动重连间隔秒 */
|
|
int pssh_ssh_reconnect_interval(void) { return g_reconnect; }
|
|
|
|
/* 选项缺少参数时的用法提示;缺参返回 -1 */
|
|
static int opt_missing(int argc, int i, const char *opt, const char *hint) {
|
|
if (i + 1 < argc) return 0;
|
|
fprintf(stderr, "pssh: option '%s' requires an argument\n"
|
|
" %s %s\n", opt, opt, hint);
|
|
return -1;
|
|
}
|
|
|
|
/* -L/-R 解析: [bind:]port:host:hostport */
|
|
static int parse_fwd(const char *spec, fwd_spec_t *out) {
|
|
const char *c1 = strrchr(spec, ':'); /* host:hostport 前的冒号 */
|
|
if (!c1) return -1;
|
|
const char *c2 = NULL;
|
|
for (const char *p = spec; p < c1; p++) if (*p == ':') c2 = p;
|
|
if (!c2) return -1;
|
|
const char *c3 = NULL;
|
|
for (const char *p = spec; p < c2; p++) if (*p == ':') c3 = p;
|
|
if (c3) { /* bind:port:host:hostport */
|
|
size_t bl = (size_t)(c3 - spec);
|
|
if (bl >= sizeof(out->bind)) bl = sizeof(out->bind) - 1;
|
|
memcpy(out->bind, spec, bl); out->bind[bl] = '\0';
|
|
out->bind_port = atoi(c3 + 1);
|
|
} else {
|
|
snprintf(out->bind, sizeof(out->bind), "127.0.0.1");
|
|
out->bind_port = atoi(spec);
|
|
}
|
|
size_t hl = (size_t)(c1 - (c2 + 1));
|
|
if (hl >= sizeof(out->target)) hl = sizeof(out->target) - 1;
|
|
memcpy(out->target, c2 + 1, hl); out->target[hl] = '\0';
|
|
out->target_port = atoi(c1 + 1);
|
|
return (out->bind_port > 0 && out->target_port > 0) ? 0 : -1;
|
|
}
|
|
|
|
/* [user@]host[:port] 解析 */
|
|
static void parse_userhost(const char *spec, char *user, size_t ulen,
|
|
char *host, size_t hlen, int *port) {
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "%s", spec);
|
|
char *at = strchr(buf, '@');
|
|
char *hs = buf;
|
|
if (user && ulen) user[0] = '\0';
|
|
if (at) {
|
|
*at = '\0';
|
|
if (user && ulen) {
|
|
size_t sl = strlen(buf);
|
|
if (sl >= ulen) sl = ulen - 1;
|
|
memcpy(user, buf, sl);
|
|
user[sl] = '\0';
|
|
}
|
|
hs = at + 1;
|
|
}
|
|
char *colon = strchr(hs, ':');
|
|
if (colon) { *colon = '\0'; if (port) *port = atoi(colon + 1); }
|
|
{
|
|
size_t sl = strlen(hs);
|
|
if (sl >= hlen) sl = hlen - 1;
|
|
memcpy(host, hs, sl);
|
|
host[sl] = '\0';
|
|
}
|
|
}
|
|
|
|
/* 交互输入密码(以 * 显示) */
|
|
static void input_password(char *pw, size_t n) {
|
|
fprintf(stderr, "Password: ");
|
|
int idx = 0, ch;
|
|
while (idx < (int)n - 1) {
|
|
ch = GETCH();
|
|
if (ch == '\r' || ch == '\n') break;
|
|
if (ch == '\b' || ch == 0x7f) {
|
|
if (idx > 0) { idx--; fprintf(stderr, "\b \b"); }
|
|
continue;
|
|
}
|
|
if (ch < 32) continue;
|
|
pw[idx++] = (char)ch;
|
|
fprintf(stderr, "*");
|
|
}
|
|
pw[idx] = '\0';
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
/* 认证:优先 -i 私钥,否则 agent,最后回退密码。返回 1=成功。 */
|
|
static int auth_session(ssh_session_t *s, const char *user,
|
|
const char *password, const char *keyfile) {
|
|
uint8_t *keydata = NULL; size_t keylen = 0;
|
|
if (keyfile) {
|
|
FILE *kf = fopen(keyfile, "rb");
|
|
if (kf) {
|
|
fseek(kf, 0, SEEK_END);
|
|
long sz = ftell(kf);
|
|
rewind(kf);
|
|
if (sz > 0 && sz < 4 * 1024 * 1024) {
|
|
keydata = (uint8_t *)malloc((size_t)sz);
|
|
if (keydata && fread(keydata, 1, (size_t)sz, kf) != (size_t)sz) {
|
|
free(keydata); keydata = NULL;
|
|
} else keylen = (size_t)sz;
|
|
}
|
|
fclose(kf);
|
|
}
|
|
if (!keydata)
|
|
fprintf(stderr, "pssh: warning: cannot read key file %s\n", keyfile);
|
|
}
|
|
int ok = 0;
|
|
if (keyfile) {
|
|
ok = ssh_auth_client_loop(s, user, password, keydata, keylen) == 0;
|
|
} else {
|
|
ssh_agent_t *agent = NULL;
|
|
if (ssh_agent_connect(&agent) == 0) {
|
|
ok = ssh_auth_client_agent_loop(s, user, password, agent) == 0;
|
|
ssh_agent_close(agent);
|
|
}
|
|
if (!ok)
|
|
ok = ssh_auth_client_loop(s, user, password, NULL, 0) == 0;
|
|
}
|
|
free(keydata);
|
|
return ok;
|
|
}
|
|
|
|
/* ---------------- 代理跳转 (-J / -J h1,h2):跳板通道上的嵌套会话 ---------------- */
|
|
|
|
typedef struct jump_ctx {
|
|
ssh_session_t *jump; /* 本层 io 数据收发的承载会话 */
|
|
uint32_t ch;
|
|
ssh_session_t *owned_session; /* 随本 ctx 一并释放的下一跳承载会话(非最顶层) */
|
|
struct jump_ctx *outer; /* 更外层 io ctx(资源释放链) */
|
|
} jump_ctx_t;
|
|
|
|
static int jump_read(void *ctx, uint8_t *buf, size_t n) {
|
|
jump_ctx_t *jc = (jump_ctx_t *)ctx;
|
|
size_t got = 0;
|
|
while (got < n) {
|
|
size_t avail = n - got;
|
|
uint32_t rid;
|
|
if (ssh_channel_recv_data(jc->jump, &rid, buf + got, &avail) < 0)
|
|
return -1;
|
|
got += avail;
|
|
/* avail==0:消费了非数据包(如 WINDOW_ADJUST),继续等数据 */
|
|
}
|
|
return (int)got;
|
|
}
|
|
|
|
static int jump_write(void *ctx, const uint8_t *buf, size_t n) {
|
|
jump_ctx_t *jc = (jump_ctx_t *)ctx;
|
|
return ssh_channel_send_data(jc->jump, jc->ch, buf, n) < 0 ? -1 : (int)n;
|
|
}
|
|
|
|
/* 一个跳板实体:[user@]host[:port] */
|
|
typedef struct {
|
|
char user[128];
|
|
char host[256];
|
|
int port;
|
|
} jump_hop_t;
|
|
|
|
#define PSSH_MAX_JUMPS 8
|
|
|
|
/* 解析 -J 逗号分隔链: "h1,h2@u:port,..."。返回跳数(0=无)。 */
|
|
static int parse_jumps(const char *spec, jump_hop_t *hops, int maxhops) {
|
|
if (!spec || !*spec) return 0;
|
|
int n = 0;
|
|
const char *start = spec;
|
|
for (const char *p = spec; ; p++) {
|
|
if (*p == ',' || *p == '\0') {
|
|
if (n >= maxhops) break;
|
|
size_t len = (size_t)(p - start);
|
|
char buf[512];
|
|
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
|
|
memcpy(buf, start, len); buf[len] = '\0';
|
|
int port = 22;
|
|
char user[128] = "", host[256] = "";
|
|
parse_userhost(buf, user, sizeof(user), host, sizeof(host), &port);
|
|
if (!host[0]) break;
|
|
snprintf(hops[n].user, sizeof(hops[n].user), "%s", user);
|
|
snprintf(hops[n].host, sizeof(hops[n].host), "%s", host);
|
|
hops[n].port = port;
|
|
n++;
|
|
if (*p == '\0') break;
|
|
start = p + 1;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* 连接目标(可选经链式跳板 -J h1,h2,...)。
|
|
* jump_out 输出最顶层跳板会话(有真实 socket,供事件循环轮询),
|
|
* 由调用方 ssh_session_free 释放。
|
|
* 返回已通过握手、未认证的目标会话 s;若 s 经跳板建立,其 io ctx(jump_ctx 链)
|
|
* 一并承载各中间/最后跳板会话的释放,调用方 free(ssh_session_io_ctx(s)) 时回收。 */
|
|
static ssh_session_t *connect_target(const char *host, int port,
|
|
const char *jump_spec,
|
|
const char *user, const char *password,
|
|
const char *keyfile, int hkmode,
|
|
ssh_session_t **jump_out) {
|
|
*jump_out = NULL;
|
|
jump_hop_t hops[PSSH_MAX_JUMPS];
|
|
int nhops = parse_jumps(jump_spec, hops, PSSH_MAX_JUMPS);
|
|
if (nhops > 1) {
|
|
/* 多级级联跳板在当前同步 I/O 架构下的应用数据阶段交互不稳定,
|
|
* 如实降级为「仅第一跳」的稳定单跳,并明确告知用户。 */
|
|
fprintf(stderr, "pssh: warning: chained multi-hop ProxyJump (%d hops) is unsupported; "
|
|
"using only the first jump %s@%s:%d\n",
|
|
nhops, hops[0].user[0] ? hops[0].user : (user ? user : ""),
|
|
hops[0].host, hops[0].port);
|
|
nhops = 1;
|
|
}
|
|
|
|
ssh_session_t *s = ssh_session_new(0);
|
|
if (!s) return NULL;
|
|
ssh_session_set_hostkey_check(s, hkmode);
|
|
ssh_session_set_verbose(s, g_verbose);
|
|
ssh_session_set_cipher(s, g_cipher);
|
|
|
|
if (nhops == 0) {
|
|
if (ssh_client_connect(s, host, port) < 0) {
|
|
fprintf(stderr, "pssh: connection failed\n");
|
|
ssh_session_free(s); return NULL;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* ---------- 单跳代理(jump = hops[0]) ---------- */
|
|
char uh[256];
|
|
if (hops[0].user[0]) {
|
|
size_t ul = strlen(hops[0].user);
|
|
if (ul >= sizeof(uh)) ul = sizeof(uh) - 1;
|
|
memcpy(uh, hops[0].user, ul); uh[ul] = '\0';
|
|
} else {
|
|
size_t ul = strlen(user);
|
|
if (ul >= sizeof(uh)) ul = sizeof(uh) - 1;
|
|
memcpy(uh, user, ul); uh[ul] = '\0';
|
|
}
|
|
fprintf(stderr, "pssh: proxyjump %s@%s:%d\n", uh, hops[0].host, hops[0].port);
|
|
|
|
ssh_session_t *j = ssh_session_new(0);
|
|
if (!j) { ssh_session_free(s); return NULL; }
|
|
ssh_session_set_hostkey_check(j, hkmode);
|
|
ssh_session_set_verbose(j, g_verbose);
|
|
ssh_session_set_cipher(j, g_cipher);
|
|
if (ssh_client_connect(j, hops[0].host, (uint16_t)hops[0].port) < 0) {
|
|
fprintf(stderr, "pssh: proxyjump: connect %s failed\n", hops[0].host);
|
|
ssh_session_free(j); ssh_session_free(s); return NULL;
|
|
}
|
|
if (!auth_session(j, uh, password, keyfile)) {
|
|
fprintf(stderr, "pssh: proxyjump: auth %s failed\n", hops[0].host);
|
|
ssh_session_free(j); ssh_session_free(s); return NULL;
|
|
}
|
|
|
|
uint32_t dich;
|
|
if (ssh_channel_open_direct(j, &dich, host, (uint32_t)port,
|
|
"127.0.0.1", 0) < 0) {
|
|
fprintf(stderr, "pssh: proxyjump: open channel to %s:%d failed\n",
|
|
host, port);
|
|
ssh_session_free(j); ssh_session_free(s); return NULL;
|
|
}
|
|
jump_ctx_t *jc = (jump_ctx_t *)malloc(sizeof(jump_ctx_t));
|
|
if (!jc) { ssh_session_free(j); ssh_session_free(s); return NULL; }
|
|
jc->jump = j;
|
|
jc->ch = dich;
|
|
jc->owned_session = NULL;
|
|
jc->outer = NULL;
|
|
ssh_session_set_io(s, jc, jump_read, jump_write);
|
|
if (ssh_client_connect_io(s, host, port) < 0) {
|
|
fprintf(stderr, "pssh: proxyjump: handshake to %s failed\n", host);
|
|
free(jc); ssh_session_free(j); ssh_session_free(s); return NULL;
|
|
}
|
|
*jump_out = j;
|
|
return s;
|
|
}
|
|
|
|
int pssh_cmd_ssh(int argc, char **argv) {
|
|
char *host = NULL;
|
|
int port = 22;
|
|
const char *user = NULL, *cmd = NULL;
|
|
const char *password_arg = NULL;
|
|
const char *keyfile = NULL;
|
|
int hostkey_mode = 0;
|
|
const char *jump_spec = NULL;
|
|
int server_alive = 15; /* ServerAliveInterval 秒;0=禁用 keepalive */
|
|
fwd_spec_t fwds[16];
|
|
int nfwds = 0;
|
|
char arg_user[128], arg_host[256]; /* [user@]host[:port] 解析缓冲 */
|
|
|
|
/* -W host:port:stdio 转发(不申请 pty/shell,把目标 TCP 隧道桥接到 stdin/stdout)。
|
|
* -N:不执行远程命令、不建会话通道,仅做端口转发(需配合 -L/-R/-D)。 */
|
|
const char *stdio_target = NULL;
|
|
int stdio_port = 0;
|
|
int no_command = 0;
|
|
int agent_fwd = 0; /* -A:agent 转发(POSIX;Windows agent 为命名管道不支持) */
|
|
char stdio_host[256];
|
|
int reconnect_flag = 0; /* 断线标记(供自动重连) */
|
|
const char *user_cfg = NULL; /* -F <file>:显式指定 config 文件 */
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
const char *a = argv[i];
|
|
if (strcmp(a, "-V") == 0) {
|
|
fprintf(stderr, "pssh %s\n", PSSH_VERSION);
|
|
return 0;
|
|
}
|
|
else if (strcmp(a, "-v") == 0) { g_verbose = 1; }
|
|
else if (strcmp(a, "-c") == 0) {
|
|
if (opt_missing(argc, i, "-c",
|
|
"<cipher> (chacha20-poly1305@openssh.com|aes128-gcm@openssh.com|aes256-gcm@openssh.com)") < 0) return 1;
|
|
const char *c = argv[++i];
|
|
if (strcmp(c, "chacha20-poly1305@openssh.com") != 0 &&
|
|
strcmp(c, "aes128-gcm@openssh.com") != 0 &&
|
|
strcmp(c, "aes256-gcm@openssh.com") != 0) {
|
|
fprintf(stderr, "pssh: unsupported cipher '%s' "
|
|
"(chacha20-poly1305@openssh.com, aes128-gcm@openssh.com, "
|
|
"aes256-gcm@openssh.com)\n", c);
|
|
return 1;
|
|
}
|
|
g_cipher = c;
|
|
}
|
|
else if (strcmp(a, "-p") == 0) {
|
|
if (opt_missing(argc, i, "-p", "<port>") < 0) return 1;
|
|
port = atoi(argv[++i]);
|
|
}
|
|
else if (strncmp(a, "-p", 2) == 0 && strlen(a) > 2 && strncmp(a, "-pwd", 4) != 0) { port = atoi(a + 2); }
|
|
else if (strcmp(a, "-pwd") == 0) {
|
|
if (opt_missing(argc, i, "-pwd", "<password>") < 0) return 1;
|
|
password_arg = argv[++i];
|
|
}
|
|
else if (strncmp(a, "-pwd=", 5) == 0) { password_arg = a + 5; }
|
|
else if (strcmp(a, "-l") == 0) {
|
|
if (opt_missing(argc, i, "-l", "<user>") < 0) return 1;
|
|
user = argv[++i];
|
|
}
|
|
else if (strcmp(a, "-i") == 0) {
|
|
if (opt_missing(argc, i, "-i", "<keyfile>") < 0) return 1;
|
|
keyfile = argv[++i];
|
|
}
|
|
else if (strcmp(a, "-J") == 0) {
|
|
if (opt_missing(argc, i, "-J", "<user@host[:port]>") < 0) return 1;
|
|
jump_spec = argv[++i];
|
|
}
|
|
else if (strcmp(a, "-W") == 0) {
|
|
if (opt_missing(argc, i, "-W", "host:port") < 0) return 1;
|
|
const char *spec = argv[++i];
|
|
const char *colon = strchr(spec, ':');
|
|
if (!colon) {
|
|
fprintf(stderr, "pssh: bad -W spec '%s' (host:port)\n", spec);
|
|
return 1;
|
|
}
|
|
size_t hl = (size_t)(colon - spec);
|
|
if (hl >= sizeof(stdio_host)) hl = sizeof(stdio_host) - 1;
|
|
memcpy(stdio_host, spec, hl); stdio_host[hl] = '\0';
|
|
stdio_port = atoi(colon + 1);
|
|
if (stdio_host[0] && stdio_port > 0) stdio_target = stdio_host;
|
|
}
|
|
else if (strcmp(a, "-N") == 0) {
|
|
no_command = 1; /* 仅转发,不执行命令/不建会话 */
|
|
}
|
|
else if (strcmp(a, "-A") == 0) {
|
|
agent_fwd = 1; /* 开启 agent 转发(POSIX) */
|
|
}
|
|
else if (strcmp(a, "-L") == 0) {
|
|
if (opt_missing(argc, i, "-L", "[bind:]port:host:hostport") < 0) return 1;
|
|
if (nfwds < 16 && parse_fwd(argv[++i], &fwds[nfwds]) == 0) {
|
|
fwds[nfwds].kind = SSH_FWD_L;
|
|
nfwds++;
|
|
}
|
|
} else if (strcmp(a, "-R") == 0) {
|
|
if (opt_missing(argc, i, "-R", "[bind:]port:host:hostport") < 0) return 1;
|
|
if (nfwds < 16 && parse_fwd(argv[++i], &fwds[nfwds]) == 0) {
|
|
fwds[nfwds].kind = SSH_FWD_R;
|
|
nfwds++;
|
|
}
|
|
} else if (strcmp(a, "-D") == 0) {
|
|
if (opt_missing(argc, i, "-D", "<port>") < 0) return 1;
|
|
if (nfwds < 16) {
|
|
fwds[nfwds].kind = SSH_FWD_D;
|
|
snprintf(fwds[nfwds].bind, sizeof(fwds[nfwds].bind), "127.0.0.1");
|
|
fwds[nfwds].bind_port = atoi(argv[++i]);
|
|
fwds[nfwds].target[0] = '\0';
|
|
fwds[nfwds].target_port = 0;
|
|
if (fwds[nfwds].bind_port > 0) nfwds++;
|
|
}
|
|
} else if (strcmp(a, "-F") == 0) {
|
|
if (opt_missing(argc, i, "-F", "<configfile>") < 0) return 1;
|
|
user_cfg = argv[++i];
|
|
} else if (strcmp(a, "-o") == 0) {
|
|
if (opt_missing(argc, i, "-o", "StrictHostKeyChecking=no|accept-new|yes|ServerAliveInterval=N") < 0) return 1;
|
|
const char *kv = argv[++i];
|
|
if (strncmp(kv, "StrictHostKeyChecking=", 22) == 0) {
|
|
const char *v = kv + 22;
|
|
if (strcmp(v, "no") == 0 || strcmp(v, "off") == 0) hostkey_mode = 2;
|
|
else if (strcmp(v, "accept-new") == 0) hostkey_mode = 1;
|
|
else hostkey_mode = 0;
|
|
}
|
|
else if (strncmp(kv, "ServerAliveInterval=", 20) == 0) {
|
|
server_alive = atoi(kv + 20);
|
|
}
|
|
else if (strncmp(kv, "ReconnectInterval=", 18) == 0) {
|
|
g_reconnect = atoi(kv + 18); /* 断线自动重连间隔秒 */
|
|
}
|
|
} else if (a[0] == '-') {
|
|
continue;
|
|
} else if (!host) {
|
|
/* [user@]host[:port]:端口随主机一起解析(如 root@host:6666) */
|
|
int p2 = 0;
|
|
parse_userhost(argv[i], arg_user, sizeof(arg_user),
|
|
arg_host, sizeof(arg_host), &p2);
|
|
if (arg_user[0]) user = arg_user;
|
|
if (p2 > 0) port = p2;
|
|
host = arg_host;
|
|
} else {
|
|
cmd = argv[i];
|
|
}
|
|
}
|
|
|
|
/* ---- config 文件:优先 -F > ~/.ssh/config(OpenSSH 约定) > ~/.pssh/config.conf ---- */
|
|
char cfg_path[1024];
|
|
const char *home = getenv("USERPROFILE");
|
|
if (!home) home = getenv("HOME");
|
|
if (user_cfg) {
|
|
snprintf(cfg_path, sizeof(cfg_path), "%s", user_cfg);
|
|
} else if (home) {
|
|
FILE *probe = NULL;
|
|
snprintf(cfg_path, sizeof(cfg_path), "%s/.ssh/config", home);
|
|
probe = fopen(cfg_path, "r");
|
|
if (probe) fclose(probe);
|
|
else snprintf(cfg_path, sizeof(cfg_path), "%s/.pssh/config.conf", home);
|
|
} else {
|
|
snprintf(cfg_path, sizeof(cfg_path), ".pssh/config.conf");
|
|
}
|
|
char cfg_host[256];
|
|
if (host) {
|
|
pssh_config_entry_t *cfgs = NULL;
|
|
int ncfg = 0;
|
|
if (pssh_config_load(cfg_path, &cfgs, &ncfg) >= 0) {
|
|
pssh_config_entry_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
m.hostkey_mode = -1;
|
|
if (pssh_config_match(cfgs, ncfg, host, &m) > 0) {
|
|
if (m.hostname[0]) {
|
|
snprintf(cfg_host, sizeof(cfg_host), "%s", m.hostname);
|
|
host = cfg_host;
|
|
}
|
|
if (m.user[0] && !user) user = m.user;
|
|
if (m.port != 0 && port == 22) port = m.port;
|
|
if (m.identity_file[0] && !keyfile) keyfile = m.identity_file;
|
|
if (m.proxyjump[0] && !jump_spec) jump_spec = m.proxyjump;
|
|
if (m.hostkey_mode != -1) hostkey_mode = m.hostkey_mode;
|
|
}
|
|
free(cfgs);
|
|
}
|
|
}
|
|
|
|
if (!host) {
|
|
fprintf(stderr, "pssh - SSH2 客户端 (平替 ssh)\n"
|
|
"用法: pssh [选项] [user@]host [command]\n"
|
|
"选项: -p <port> -l <user> -pwd <pw> -i <key> -J <user@jump[:port]>\n"
|
|
" -W host:port -N(仅转发) -L [bind:]port:host:hostport\n"
|
|
" -R [bind:]port:host:hostport -D <port>\n"
|
|
" -o StrictHostKeyChecking=no|accept-new|yes\n");
|
|
return 1;
|
|
}
|
|
if (!user) {
|
|
const char *envu = getenv("USER");
|
|
if (!envu) envu = getenv("USERNAME");
|
|
user = envu ? envu : "root";
|
|
}
|
|
|
|
char pw_buf[256];
|
|
const char *password = password_arg;
|
|
if (!password) {
|
|
input_password(pw_buf, sizeof(pw_buf));
|
|
password = pw_buf;
|
|
}
|
|
|
|
/* ---- 连接(可选经跳板) + 认证 ---- */
|
|
ssh_session_t *jump = NULL;
|
|
ssh_session_t *s = connect_target(host, port, jump_spec, user, password,
|
|
keyfile, hostkey_mode, &jump);
|
|
if (!s) return 1;
|
|
if (!auth_session(s, user, password, keyfile)) {
|
|
fprintf(stderr, "pssh: auth failed\n");
|
|
if (jump) {
|
|
free(ssh_session_io_ctx(s)); /* 释放跳板 io ctx 链(含中间会话) */
|
|
ssh_session_free(jump); /* 释放顶层跳板 */
|
|
}
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
|
|
/* ---- 建立转发 ---- */
|
|
ssh_fwd_t *fwd = ssh_fwd_new();
|
|
for (int i = 0; i < nfwds; i++) {
|
|
fwd_spec_t *fs = &fwds[i];
|
|
if (fs->kind == SSH_FWD_R) {
|
|
uint32_t ap = 0;
|
|
ssh_fwd_add_remote(fwd, s, fs->bind, fs->bind_port,
|
|
fs->target, fs->target_port, &ap);
|
|
} else {
|
|
ssh_fwd_add_local(fwd, s, fs->kind, fs->bind, fs->bind_port,
|
|
fs->target, fs->target_port);
|
|
}
|
|
}
|
|
|
|
/* ---- 会话通道 ---- */
|
|
uint32_t ch = 0;
|
|
int interactive = 0;
|
|
int stdio_mode = 0; /* -W:目标 TCP 通道桥接到本地 stdin/stdout */
|
|
#ifndef _WIN32
|
|
int shell_raw = 0; /* 是否已调用 raw_on(普通交互 shell;仅 POSIX) */
|
|
#endif
|
|
if (stdio_target) {
|
|
/* -W host:port:直接对目标开 direct-tcpip 通道,桥接本地 stdio */
|
|
if (ssh_channel_open_direct(s, &ch, stdio_target, (uint32_t)stdio_port,
|
|
"127.0.0.1", 0) < 0) {
|
|
fprintf(stderr, "pssh: -W: open channel to %s:%d failed\n",
|
|
stdio_target, stdio_port);
|
|
goto done;
|
|
}
|
|
stdio_mode = 1;
|
|
interactive = 1; /* 读 stdin 转发到通道 */
|
|
} else if (cmd) {
|
|
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
|
|
fprintf(stderr, "pssh: channel open failed\n");
|
|
goto done;
|
|
}
|
|
ssh_channel_request_exec(s, ch, cmd);
|
|
} else if (no_command) {
|
|
/* -N:不建会话通道,仅端口转发 */
|
|
interactive = 0; /* 事件循环只泵转发,不读 stdin */
|
|
} else {
|
|
interactive = 1;
|
|
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
|
|
fprintf(stderr, "pssh: channel open failed\n");
|
|
goto done;
|
|
}
|
|
#ifdef _WIN32
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
#else
|
|
raw_on();
|
|
shell_raw = 1;
|
|
#endif
|
|
ssh_channel_request_pty(s, ch, "xterm", 80, 24);
|
|
ssh_channel_request_shell(s, ch);
|
|
}
|
|
|
|
/* ---- agent 转发(-A, POSIX):建立 auth-agent@openssh.com 通道 + 本地 agent ---- */
|
|
ssh_agent_t *ag = NULL; uint32_t ach = 0; int ag_fd = -1;
|
|
if (agent_fwd) {
|
|
ag_fd = -1;
|
|
if (ssh_agent_connect(&ag) == 0) {
|
|
ag_fd = ssh_agent_fd(ag);
|
|
if (ag_fd < 0) { /* Windows:命名管道不支持 select,禁用 */
|
|
ssh_agent_close(ag); ag = NULL;
|
|
fprintf(stderr, "pssh: agent 转发在 Windows 不受支持(skip -A)\n");
|
|
goto done;
|
|
}
|
|
if (ssh_channel_open(s, &ach, "auth-agent@openssh.com", 0, 0) < 0) {
|
|
ssh_agent_close(ag); ag = NULL;
|
|
fprintf(stderr, "pssh: agent forwarding channel open failed (server may not support)\n");
|
|
goto done;
|
|
}
|
|
} else {
|
|
fprintf(stderr, "pssh: cannot connect local agent (ssh-agent not running)\n");
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* ---- 事件循环 ---- */
|
|
long sel_sock = jump ? ssh_session_socket(jump) : ssh_session_socket(s);
|
|
if (sel_sock < 0) goto done;
|
|
|
|
uint8_t rbuf[32768];
|
|
time_t last_alive = time(NULL);
|
|
for (;;) {
|
|
/* 定时 keepalive(ServerAliveInterval):保持空闲连接存活 */
|
|
if (server_alive > 0) {
|
|
time_t now = time(NULL);
|
|
if (now - last_alive >= server_alive) {
|
|
last_alive = now;
|
|
if (ssh_keepalive_send(s) < 0) { fprintf(stderr, "pssh: keepalive send failed\n"); reconnect_flag = 1; break; }
|
|
}
|
|
}
|
|
fd_set rfds;
|
|
FD_ZERO(&rfds);
|
|
int maxfd = 0;
|
|
FD_SET((SOCKET)sel_sock, &rfds);
|
|
if ((int)sel_sock > maxfd) maxfd = (int)sel_sock;
|
|
if (interactive) {
|
|
#ifndef _WIN32
|
|
FD_SET(STDIN_FD, &rfds);
|
|
if (STDIN_FD > maxfd) maxfd = STDIN_FD;
|
|
#endif
|
|
}
|
|
ssh_fwd_prepare(fwd, &rfds, NULL, &maxfd);
|
|
#ifndef _WIN32
|
|
if (ag_fd >= 0) { FD_SET(ag_fd, &rfds); if (ag_fd > maxfd) maxfd = ag_fd; }
|
|
#endif
|
|
|
|
struct timeval tv = {0, 100000};
|
|
int sret = select(maxfd + 1, &rfds, NULL, NULL, &tv);
|
|
if (sret < 0) break;
|
|
|
|
#ifndef _WIN32
|
|
/* agent 转发:本地 agent fd 可读 → 转发到 auth-agent 通道 */
|
|
if (ag_fd >= 0 && sret > 0 && FD_ISSET(ag_fd, &rfds)) {
|
|
uint8_t abuf[8192];
|
|
ssize_t an = read(ag_fd, abuf, sizeof(abuf));
|
|
if (an < 0) break;
|
|
if (an > 0 && ssh_channel_send_data(s, ach, abuf, (size_t)an) < 0) break;
|
|
}
|
|
#endif
|
|
|
|
if (sret > 0 && FD_ISSET((SOCKET)sel_sock, &rfds)) {
|
|
/* 先消费服务器主动推送的 forwarded-tcpip 通道打开。
|
|
网关同时看内层 pending 与外层缓冲/socket,防止 -J 场景
|
|
内层已缓存但外层 socket 空的包被滞留。 */
|
|
for (;;) {
|
|
if (ssh_session_data_ready(s) <= 0 &&
|
|
ssh_session_data_ready(jump ? jump : s) <= 0) break;
|
|
uint32_t ach;
|
|
char ahost[256];
|
|
uint32_t aport;
|
|
int ar = ssh_channel_accept_forwarded(s, &ach, ahost,
|
|
sizeof(ahost), &aport);
|
|
if (ar == 0) {
|
|
if (ssh_fwd_remote_connect(fwd, s, ach, ahost, aport) < 0)
|
|
ssh_channel_close(s, ach);
|
|
continue;
|
|
}
|
|
break; /* -2=非 OPEN 留给数据循环; <0=错误 */
|
|
}
|
|
/* 收通道数据并分发 */
|
|
for (;;) {
|
|
if (ssh_session_data_ready(s) <= 0 &&
|
|
ssh_session_data_ready(jump ? jump : s) <= 0) break;
|
|
uint32_t rid;
|
|
size_t n = sizeof(rbuf);
|
|
int r = ssh_channel_recv_data(s, &rid, rbuf, &n);
|
|
if (r < 0) {
|
|
/* 通道关闭/EOF:主会话通道 → 结束;转发通道 → 清理后继续 */
|
|
if (rid == ch) goto done;
|
|
ssh_fwd_channel_closed(fwd, rid);
|
|
continue;
|
|
}
|
|
if (n == 0) {
|
|
/* 控制包已消费;先取 pending 中可能有的 forwarded-open */
|
|
while (ssh_session_data_ready(s) > 0) {
|
|
uint32_t ach;
|
|
char ahost[256];
|
|
uint32_t aport;
|
|
int ar = ssh_channel_accept_forwarded(s, &ach, ahost,
|
|
sizeof(ahost), &aport);
|
|
if (ar == 0) {
|
|
if (ssh_fwd_remote_connect(fwd, s, ach, ahost, aport) < 0)
|
|
ssh_channel_close(s, ach);
|
|
continue;
|
|
}
|
|
break; /* -2: 非 OPEN 留在 pending; <0: 错误 */
|
|
}
|
|
/* 内层 pending / 外层缓冲或 socket 还有数据 → 继续收 */
|
|
if (ssh_session_data_ready(s) > 0 ||
|
|
ssh_session_data_ready(jump ? jump : s) > 0)
|
|
continue;
|
|
break;
|
|
}
|
|
if (rid == ch) {
|
|
fwrite(rbuf, 1, n, stdout);
|
|
fflush(stdout);
|
|
#ifndef _WIN32
|
|
} else if (ag_fd >= 0 && rid == ach) {
|
|
/* agent 通道数据 → 写本地 agent socket */
|
|
ssize_t wc = write(ag_fd, rbuf, n);
|
|
if (wc < 0) goto done;
|
|
#endif
|
|
} else {
|
|
ssh_fwd_channel_data(fwd, rid, rbuf, n);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 转发本地事件 */
|
|
ssh_fwd_pump(fwd, s, &rfds, NULL);
|
|
|
|
/* 键盘输入(交互 / -W stdio 转发) */
|
|
if (interactive) {
|
|
#ifdef _WIN32
|
|
if (stdio_mode) {
|
|
/* -W:低位字节透传,不映射转义键 */
|
|
while (KBHIT()) {
|
|
uint8_t b = (uint8_t)GETCH();
|
|
if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
|
|
}
|
|
} else {
|
|
while (KBHIT()) {
|
|
int c = GETCH();
|
|
if (c == 0 || c == 0xE0) {
|
|
int sc = GETCH();
|
|
uint8_t seq[3];
|
|
size_t n = 0;
|
|
switch (sc) {
|
|
case 72: seq[0]=0x1b; seq[1]='['; seq[2]='A'; n=3; break;
|
|
case 80: seq[0]=0x1b; seq[1]='['; seq[2]='B'; n=3; break;
|
|
case 75: seq[0]=0x1b; seq[1]='['; seq[2]='D'; n=3; break;
|
|
case 77: seq[0]=0x1b; seq[1]='['; seq[2]='C'; n=3; break;
|
|
default: break;
|
|
}
|
|
if (n) ssh_channel_send_data(s, ch, seq, n);
|
|
continue;
|
|
}
|
|
uint8_t b = (uint8_t)c;
|
|
if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
|
|
}
|
|
}
|
|
#else
|
|
fd_set ifds;
|
|
FD_ZERO(&ifds);
|
|
FD_SET(STDIN_FILENO, &ifds);
|
|
struct timeval itv = {0, 0};
|
|
if (select(STDIN_FILENO + 1, &ifds, NULL, NULL, &itv) > 0) {
|
|
uint8_t kb;
|
|
if (read(STDIN_FILENO, &kb, 1) == 1)
|
|
if (ssh_channel_send_data(s, ch, &kb, 1) < 0) goto done;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
done:
|
|
int exit_code = 0;
|
|
if (ch) ssh_channel_exit_status(s, ch, &exit_code);
|
|
#ifndef _WIN32
|
|
if (shell_raw) raw_off();
|
|
#endif
|
|
ssh_fwd_free(fwd);
|
|
if (ag) { ssh_agent_close(ag); ag = NULL; } /* agent 转发清理 */
|
|
if (jump) {
|
|
free(ssh_session_io_ctx(s)); /* jump_ctx_t */
|
|
ssh_session_free(jump);
|
|
}
|
|
ssh_session_free(s);
|
|
if (reconnect_flag && g_reconnect > 0) return -2; /* 通知上层自动重连 */
|
|
return exit_code;
|
|
}
|