TLS 1.3 PSK 会话恢复: NewSessionTicket 签发/解析、ticket+binder 校验、selected_identity 回选、恢复握手免证书(pazessl -sess_in/-sess_out 端到端验证); 构建脚本输出 bin/ 并支持独立命令与 psftp; 修复 SFTP 二进制传输与 copy_id -P; 补充 tests/ 调试与 verify_tls
710 行
24 KiB
C
710 行
24 KiB
C
/* psftp —— 独立 SFTP 客户端 (平替 OpenSSH sftp)
|
|
*
|
|
* 用法:
|
|
* psftp [选项] [user@]host[:初始路径]
|
|
* -P <port> 远程端口 (默认 22)
|
|
* -pwd <pw> 密码
|
|
* -i <key> 私钥文件 (publickey 认证)
|
|
* -o <opt> StrictHostKeyChecking=no|accept-new|yes
|
|
* -b <file> 批处理:逐行执行命令后退出
|
|
*
|
|
* 交互命令:
|
|
* bye/exit/quit 退出
|
|
* pwd/lpwd 显示远端/本地当前目录
|
|
* cd/lcd <path> 切换远端/本地目录
|
|
* ls [path] 列出远端目录
|
|
* lls [path] 列出本地目录
|
|
* get [-r] <remote> [local] 下载(文件或目录)
|
|
* put [-r] <local> [remote] 上传(文件或目录)
|
|
* rm <path> 删除远端文件
|
|
* mkdir <path> 创建远端目录
|
|
* rmdir <path> 删除远端空目录
|
|
* rename <old> <new> 远端重命名
|
|
* help/?
|
|
*/
|
|
#include "paze/ssh.h"
|
|
#include "paze/ssh_agent.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#ifndef S_ISDIR
|
|
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
|
|
#endif
|
|
|
|
/* SFTP attrs 权限值中的文件类型掩码(与 POSIX st_mode 高 4 位一致) */
|
|
#define SSH_S_IFMT 0170000
|
|
#define SSH_S_IFDIR 0040000
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <conio.h>
|
|
#include <direct.h>
|
|
#define GETCH _getch
|
|
#define IS_SEP(c) ((c) == '/' || (c) == '\\')
|
|
static int local_mkdir(const char *p) { return _mkdir(p); }
|
|
#else
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#define IS_SEP(c) ((c) == '/')
|
|
static int local_mkdir(const char *p) { return mkdir(p, 0755); }
|
|
#endif
|
|
|
|
#define SFTP_BUF 32768
|
|
|
|
static void path_join_local(char *out, size_t n, const char *a, const char *b) {
|
|
size_t al = strlen(a);
|
|
int need = (al > 0 && !IS_SEP(a[al - 1])) ? 1 : 0;
|
|
#ifdef _WIN32
|
|
snprintf(out, n, need ? "%s\\%s" : "%s%s", a, b);
|
|
#else
|
|
snprintf(out, n, need ? "%s/%s" : "%s%s", a, b);
|
|
#endif
|
|
}
|
|
static void rjoin(char *out, size_t n, const char *base, const char *name) {
|
|
size_t bl = strlen(base);
|
|
int need = (bl > 0 && base[bl - 1] != '/') ? 1 : 0;
|
|
snprintf(out, n, need ? "%s/%s" : "%s%s", base, name);
|
|
}
|
|
|
|
static const char *path_basename(const char *p) {
|
|
const char *last = p;
|
|
size_t len = strlen(p);
|
|
while (len > 1 && IS_SEP(p[len - 1])) len--;
|
|
for (size_t i = 0; i < len; i++)
|
|
if (IS_SEP(p[i])) last = p + i + 1;
|
|
return last;
|
|
}
|
|
|
|
typedef struct { char name[512]; } ldir_ent_t;
|
|
static int local_dir_enum(const char *dir, ldir_ent_t *ents, int max) {
|
|
int n = 0;
|
|
#ifdef _WIN32
|
|
char pat[1024];
|
|
size_t dl = strlen(dir);
|
|
if (dl >= sizeof(pat) - 2) return -1;
|
|
if (dl > 0 && (dir[dl - 1] == '/' || dir[dl - 1] == '\\'))
|
|
snprintf(pat, sizeof(pat), "%s*", dir);
|
|
else
|
|
snprintf(pat, sizeof(pat), "%s\\*", dir);
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA(pat, &fd);
|
|
if (h == INVALID_HANDLE_VALUE) return -1;
|
|
do {
|
|
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
|
|
continue;
|
|
if (n < max) { snprintf(ents[n].name, sizeof(ents[n].name), "%s", fd.cFileName); n++; }
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
#else
|
|
DIR *d = opendir(dir);
|
|
if (!d) return -1;
|
|
struct dirent *e;
|
|
while ((e = readdir(d)) != NULL) {
|
|
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
|
continue;
|
|
if (n < max) { snprintf(ents[n].name, sizeof(ents[n].name), "%s", e->d_name); n++; }
|
|
}
|
|
closedir(d);
|
|
#endif
|
|
return n;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 连接 + 认证
|
|
* ============================================================ */
|
|
typedef struct {
|
|
int port;
|
|
const char *user, *password_arg, *keyfile;
|
|
int hostkey_mode;
|
|
} cli_cfg_t;
|
|
|
|
static ssh_session_t *connect_session(const cli_cfg_t *cfg,
|
|
const char *host, const char *ruser) {
|
|
const char *user = ruser ? ruser : cfg->user;
|
|
if (!user) {
|
|
const char *envu = getenv("USER");
|
|
if (!envu) envu = getenv("USERNAME");
|
|
user = envu ? envu : "root";
|
|
}
|
|
|
|
char pw_buf[256];
|
|
const char *password = cfg->password_arg;
|
|
if (!password) {
|
|
fprintf(stderr, "Password: ");
|
|
int idx = 0, ch;
|
|
while (idx < (int)sizeof(pw_buf) - 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_buf[idx++] = (char)ch;
|
|
fprintf(stderr, "*");
|
|
}
|
|
pw_buf[idx] = '\0';
|
|
fprintf(stderr, "\n");
|
|
password = pw_buf;
|
|
}
|
|
|
|
ssh_session_t *s = ssh_session_new(0);
|
|
if (!s) { fprintf(stderr, "psftp: 会话分配失败\n"); return NULL; }
|
|
ssh_session_set_hostkey_check(s, cfg->hostkey_mode);
|
|
if (ssh_client_connect(s, host, (uint16_t)cfg->port) < 0) {
|
|
fprintf(stderr, "psftp: 连接 %s 失败\n", host);
|
|
ssh_session_free(s);
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *keydata = NULL; size_t keylen = 0;
|
|
if (cfg->keyfile) {
|
|
FILE *kf = fopen(cfg->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);
|
|
}
|
|
}
|
|
|
|
int auth_ok = 0;
|
|
if (keydata) {
|
|
auth_ok = ssh_auth_client_loop(s, user, password, keydata, keylen) == 0;
|
|
free(keydata);
|
|
} else {
|
|
ssh_agent_t *agent = NULL;
|
|
if (ssh_agent_connect(&agent) == 0) {
|
|
auth_ok = ssh_auth_client_agent_loop(s, user, password, agent) == 0;
|
|
ssh_agent_close(agent);
|
|
}
|
|
if (!auth_ok)
|
|
auth_ok = ssh_auth_client_loop(s, user, password, NULL, 0) == 0;
|
|
}
|
|
if (!auth_ok) {
|
|
fprintf(stderr, "psftp: 认证失败\n");
|
|
ssh_session_free(s);
|
|
return NULL;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 传输 (文件 / 目录递归)
|
|
* ============================================================ */
|
|
static int sftp_get_file(ssh_sftp_t *sf, const char *remote,
|
|
const char *local) {
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_open(sf, remote, SSH_FXF_READ, handle, &hlen) < 0) {
|
|
fprintf(stderr, "psftp: 无法打开远端 %s\n", remote);
|
|
return -1;
|
|
}
|
|
FILE *f = fopen(local, "wb");
|
|
if (!f) {
|
|
fprintf(stderr, "psftp: 无法创建本地 %s\n", local);
|
|
ssh_sftp_close(sf, handle, hlen);
|
|
return -1;
|
|
}
|
|
uint64_t off = 0;
|
|
int code = 0, rc = 0;
|
|
for (;;) {
|
|
uint8_t buf[SFTP_BUF];
|
|
int r = ssh_sftp_read(sf, handle, hlen, off, buf, sizeof(buf), &code);
|
|
if (r < 0) { fprintf(stderr, "psftp: 读取 %s 失败 (code=%d)\n", remote, code); rc = -1; break; }
|
|
if (r == 0) break;
|
|
if (fwrite(buf, 1, (size_t)r, f) != (size_t)r) { rc = -1; break; }
|
|
off += (uint64_t)r;
|
|
}
|
|
fclose(f);
|
|
ssh_sftp_close(sf, handle, hlen);
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_put_file(ssh_sftp_t *sf, const char *local,
|
|
const char *remote) {
|
|
FILE *f = fopen(local, "rb");
|
|
if (!f) { fprintf(stderr, "psftp: 无法打开本地 %s\n", local); return -1; }
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_open(sf, remote, SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC,
|
|
handle, &hlen) < 0) {
|
|
fprintf(stderr, "psftp: 无法在远端创建 %s\n", remote);
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
uint64_t off = 0;
|
|
uint8_t buf[SFTP_BUF];
|
|
size_t n;
|
|
int rc = 0;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
|
size_t done = 0;
|
|
while (done < n) {
|
|
int w = ssh_sftp_write(sf, handle, hlen, off + done, buf + done,
|
|
(uint32_t)(n - done), NULL);
|
|
if (w <= 0) { rc = -1; break; }
|
|
done += (size_t)w;
|
|
}
|
|
if (rc < 0) break;
|
|
off += n;
|
|
}
|
|
fclose(f);
|
|
ssh_sftp_close(sf, handle, hlen);
|
|
return rc;
|
|
}
|
|
|
|
static void remote_mkdir_p(ssh_sftp_t *sf, const char *path) {
|
|
char tmp[2048];
|
|
size_t plen = strlen(path);
|
|
if (plen >= sizeof(tmp)) return;
|
|
const char *last = strrchr(path, '/');
|
|
if (last && last != path) {
|
|
size_t pl = (size_t)(last - path);
|
|
if (pl >= sizeof(tmp)) return;
|
|
memcpy(tmp, path, pl);
|
|
tmp[pl] = '\0';
|
|
remote_mkdir_p(sf, tmp);
|
|
}
|
|
if (ssh_sftp_mkdir(sf, path, 0755) < 0) {
|
|
ssh_sftp_ent_t e;
|
|
if (ssh_sftp_stat(sf, path, &e) == 0 &&
|
|
(e.perms & SSH_S_IFMT) == SSH_S_IFDIR)
|
|
return;
|
|
}
|
|
}
|
|
|
|
static int sftp_get_dir(ssh_sftp_t *sf, const char *remote,
|
|
const char *local) {
|
|
local_mkdir(local);
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_opendir(sf, remote, handle, &hlen) < 0) {
|
|
fprintf(stderr, "psftp: 无法打开远端目录 %s\n", remote);
|
|
return -1;
|
|
}
|
|
ssh_sftp_ent_t ent;
|
|
int rc = 0;
|
|
while (ssh_sftp_readdir(sf, handle, hlen, &ent) > 0) {
|
|
if (strcmp(ent.name, ".") == 0 || strcmp(ent.name, "..") == 0)
|
|
continue;
|
|
char lp[1024], rp[2048];
|
|
path_join_local(lp, sizeof(lp), local, ent.name);
|
|
rjoin(rp, sizeof(rp), remote, ent.name);
|
|
if ((ent.perms & SSH_S_IFMT) == SSH_S_IFDIR) {
|
|
if (sftp_get_dir(sf, rp, lp) < 0) rc = -1;
|
|
} else if (sftp_get_file(sf, rp, lp) < 0) {
|
|
rc = -1;
|
|
}
|
|
}
|
|
ssh_sftp_close(sf, handle, hlen);
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_put_dir(ssh_sftp_t *sf, const char *local,
|
|
const char *remote) {
|
|
remote_mkdir_p(sf, remote);
|
|
ldir_ent_t *ents = (ldir_ent_t *)malloc(1024 * sizeof(ldir_ent_t));
|
|
if (!ents) return -1;
|
|
int n = local_dir_enum(local, ents, 1024);
|
|
if (n < 0) { free(ents); fprintf(stderr, "psftp: 无法读取本地目录 %s\n", local); return -1; }
|
|
int rc = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
char lp[1024], rp[2048];
|
|
path_join_local(lp, sizeof(lp), local, ents[i].name);
|
|
rjoin(rp, sizeof(rp), remote, ents[i].name);
|
|
struct stat st;
|
|
if (stat(lp, &st) != 0) { rc = -1; continue; }
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (sftp_put_dir(sf, lp, rp) < 0) rc = -1;
|
|
} else if (sftp_put_file(sf, lp, rp) < 0) {
|
|
rc = -1;
|
|
}
|
|
}
|
|
free(ents);
|
|
return rc;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 交互命令
|
|
* ============================================================ */
|
|
typedef struct {
|
|
ssh_sftp_t *sf;
|
|
char rdir[2048];
|
|
char ldir[1024];
|
|
} psftp_t;
|
|
|
|
static void cmd_pwd(psftp_t *p) { printf("远端当前目录: %s\n", p->rdir); }
|
|
static void cmd_lpwd(psftp_t *p) {
|
|
#ifdef _WIN32
|
|
if (GetCurrentDirectoryA(sizeof(p->ldir), p->ldir)) printf("本地当前目录: %s\n", p->ldir);
|
|
#else
|
|
if (getcwd(p->ldir, sizeof(p->ldir))) printf("本地当前目录: %s\n", p->ldir);
|
|
#endif
|
|
}
|
|
|
|
static void cmd_cd(psftp_t *p, const char *path) {
|
|
char out[2048];
|
|
if (ssh_sftp_realpath(p->sf, path, out, sizeof(out)) < 0) {
|
|
fprintf(stderr, "psftp: cd: %s: 无此目录\n", path);
|
|
return;
|
|
}
|
|
ssh_sftp_ent_t e;
|
|
if (ssh_sftp_stat(p->sf, out, &e) < 0 ||
|
|
(e.perms & SSH_S_IFMT) != SSH_S_IFDIR) {
|
|
fprintf(stderr, "psftp: cd: %s: 无此目录\n", path);
|
|
return;
|
|
}
|
|
snprintf(p->rdir, sizeof(p->rdir), "%s", out);
|
|
}
|
|
|
|
static void cmd_lcd(psftp_t *p, const char *path) {
|
|
#ifdef _WIN32
|
|
if (SetCurrentDirectoryA(path)) {
|
|
GetCurrentDirectoryA(sizeof(p->ldir), p->ldir);
|
|
printf("本地现在使用: %s\n", p->ldir);
|
|
} else fprintf(stderr, "psftp: lcd: %s: 无此目录\n", path);
|
|
#else
|
|
if (chdir(path) == 0) {
|
|
getcwd(p->ldir, sizeof(p->ldir));
|
|
printf("本地现在使用: %s\n", p->ldir);
|
|
} else fprintf(stderr, "psftp: lcd: %s: 无此目录\n", path);
|
|
#endif
|
|
}
|
|
|
|
static void cmd_ls(psftp_t *p, const char *path) {
|
|
char rp[2048];
|
|
if (!path || !path[0]) {
|
|
snprintf(rp, sizeof(rp), "%s", p->rdir);
|
|
} else if (path[0] == '/') {
|
|
snprintf(rp, sizeof(rp), "%s", path);
|
|
} else {
|
|
rjoin(rp, sizeof(rp), p->rdir, path);
|
|
}
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_opendir(p->sf, rp, handle, &hlen) < 0) {
|
|
fprintf(stderr, "psftp: ls: %s: 无法打开\n", path ? path : ".");
|
|
return;
|
|
}
|
|
ssh_sftp_ent_t ent;
|
|
while (ssh_sftp_readdir(p->sf, handle, hlen, &ent) > 0) {
|
|
char t = ((ent.perms & SSH_S_IFMT) == SSH_S_IFDIR) ? 'd' : '-';
|
|
printf("%c %10llu %s\n", t, (unsigned long long)ent.size, ent.name);
|
|
}
|
|
ssh_sftp_close(p->sf, handle, hlen);
|
|
}
|
|
|
|
static void cmd_lls(psftp_t *p, const char *path) {
|
|
char lp[1024];
|
|
if (!path || !path[0]) {
|
|
snprintf(lp, sizeof(lp), "%s", p->ldir);
|
|
} else if (IS_SEP(path[0])) {
|
|
snprintf(lp, sizeof(lp), "%s", path);
|
|
} else {
|
|
path_join_local(lp, sizeof(lp), p->ldir, path);
|
|
}
|
|
ldir_ent_t *ents = (ldir_ent_t *)malloc(1024 * sizeof(ldir_ent_t));
|
|
if (!ents) return;
|
|
int n = local_dir_enum(lp, ents, 1024);
|
|
for (int i = 0; i < n; i++) printf("%s\n", ents[i].name);
|
|
free(ents);
|
|
}
|
|
|
|
static void cmd_get(psftp_t *p, int recursive, const char *remote,
|
|
const char *local) {
|
|
char rp[2048];
|
|
if (remote[0] == '/') snprintf(rp, sizeof(rp), "%s", remote);
|
|
else rjoin(rp, sizeof(rp), p->rdir, remote);
|
|
|
|
char lp[1024];
|
|
if (local && local[0]) {
|
|
if (IS_SEP(local[0])) snprintf(lp, sizeof(lp), "%s", local);
|
|
else path_join_local(lp, sizeof(lp), p->ldir, local);
|
|
} else {
|
|
path_join_local(lp, sizeof(lp), p->ldir, path_basename(rp));
|
|
}
|
|
|
|
ssh_sftp_ent_t e;
|
|
int is_dir = 0;
|
|
if (ssh_sftp_stat(p->sf, rp, &e) == 0)
|
|
is_dir = ((e.perms & SSH_S_IFMT) == SSH_S_IFDIR);
|
|
if (is_dir) {
|
|
if (!recursive) { fprintf(stderr, "psftp: %s 是目录,需要 -r\n", rp); return; }
|
|
if (sftp_get_dir(p->sf, rp, lp) < 0) fprintf(stderr, "psftp: get 目录失败\n");
|
|
} else {
|
|
if (sftp_get_file(p->sf, rp, lp) < 0) fprintf(stderr, "psftp: get 失败\n");
|
|
}
|
|
}
|
|
|
|
static void cmd_put(psftp_t *p, int recursive, const char *local,
|
|
const char *remote) {
|
|
char lp[1024];
|
|
if (IS_SEP(local[0])) snprintf(lp, sizeof(lp), "%s", local);
|
|
else path_join_local(lp, sizeof(lp), p->ldir, local);
|
|
|
|
char rp[2048];
|
|
if (remote && remote[0]) {
|
|
if (remote[0] == '/') snprintf(rp, sizeof(rp), "%s", remote);
|
|
else rjoin(rp, sizeof(rp), p->rdir, remote);
|
|
} else {
|
|
rjoin(rp, sizeof(rp), p->rdir, path_basename(lp));
|
|
}
|
|
|
|
struct stat st;
|
|
if (stat(lp, &st) != 0) { fprintf(stderr, "psftp: put: %s: 无此文件\n", lp); return; }
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (!recursive) { fprintf(stderr, "psftp: %s 是目录,需要 -r\n", lp); return; }
|
|
if (sftp_put_dir(p->sf, lp, rp) < 0) fprintf(stderr, "psftp: put 目录失败\n");
|
|
} else {
|
|
if (sftp_put_file(p->sf, lp, rp) < 0) fprintf(stderr, "psftp: put 失败\n");
|
|
}
|
|
}
|
|
|
|
static void cmd_rm(psftp_t *p, const char *path) {
|
|
char rp[2048];
|
|
if (path[0] == '/') snprintf(rp, sizeof(rp), "%s", path);
|
|
else rjoin(rp, sizeof(rp), p->rdir, path);
|
|
if (ssh_sftp_remove(p->sf, rp) < 0)
|
|
fprintf(stderr, "psftp: rm: %s 失败\n", rp);
|
|
}
|
|
|
|
static void cmd_mkdir(psftp_t *p, const char *path) {
|
|
char rp[2048];
|
|
if (path[0] == '/') snprintf(rp, sizeof(rp), "%s", path);
|
|
else rjoin(rp, sizeof(rp), p->rdir, path);
|
|
if (ssh_sftp_mkdir(p->sf, rp, 0755) < 0)
|
|
fprintf(stderr, "psftp: mkdir: %s 失败\n", rp);
|
|
}
|
|
|
|
static void cmd_rmdir(psftp_t *p, const char *path) {
|
|
char rp[2048];
|
|
if (path[0] == '/') snprintf(rp, sizeof(rp), "%s", path);
|
|
else rjoin(rp, sizeof(rp), p->rdir, path);
|
|
if (ssh_sftp_rmdir(p->sf, rp) < 0)
|
|
fprintf(stderr, "psftp: rmdir: %s 失败\n", rp);
|
|
}
|
|
|
|
static void cmd_rename(psftp_t *p, const char *oldp, const char *newp) {
|
|
char o[2048], nw[2048];
|
|
if (oldp[0] == '/') snprintf(o, sizeof(o), "%s", oldp);
|
|
else rjoin(o, sizeof(o), p->rdir, oldp);
|
|
if (newp[0] == '/') snprintf(nw, sizeof(nw), "%s", newp);
|
|
else rjoin(nw, sizeof(nw), p->rdir, newp);
|
|
if (ssh_sftp_rename(p->sf, o, nw) < 0)
|
|
fprintf(stderr, "psftp: rename: %s -> %s 失败\n", oldp, newp);
|
|
}
|
|
|
|
static void help(void) {
|
|
printf("可用命令:\n"
|
|
" bye, exit, quit 退出 psftp\n"
|
|
" pwd / lpwd 显示远端 / 本地当前目录\n"
|
|
" cd / lcd <path> 切换远端 / 本地目录\n"
|
|
" ls [path] 列出远端目录\n"
|
|
" lls [path] 列出本地目录\n"
|
|
" get [-r] <remote> [local] 下载(文件或目录)\n"
|
|
" put [-r] <local> [remote] 上传(文件或目录)\n"
|
|
" rm <path> 删除远端文件\n"
|
|
" mkdir <path> 创建远端目录\n"
|
|
" rmdir <path> 删除远端空目录\n"
|
|
" rename <old> <new> 远端重命名\n"
|
|
" help, ? 显示本帮助\n");
|
|
}
|
|
|
|
/* 执行单条命令;返回 1=退出 */
|
|
static int run_line(psftp_t *p, char *line) {
|
|
/* 去首尾空白 */
|
|
char *s = line;
|
|
while (*s == ' ' || *s == '\t') s++;
|
|
char *end = s + strlen(s);
|
|
while (end > s && (end[-1] == '\n' || end[-1] == '\r' || end[-1] == ' ' ||
|
|
end[-1] == '\t')) end--;
|
|
*end = '\0';
|
|
if (!*s) return 0;
|
|
|
|
char *args[8];
|
|
int nargs = 0;
|
|
char *tok = s;
|
|
while (tok && nargs < 8) {
|
|
while (*tok == ' ' || *tok == '\t') tok++;
|
|
if (!*tok) break;
|
|
args[nargs++] = tok;
|
|
char *sp = strchr(tok, ' ');
|
|
if (!sp) sp = strchr(tok, '\t');
|
|
if (!sp) break;
|
|
*sp = '\0';
|
|
tok = sp + 1;
|
|
}
|
|
|
|
const char *c = args[0];
|
|
if (strcmp(c, "bye") == 0 || strcmp(c, "exit") == 0 || strcmp(c, "quit") == 0)
|
|
return 1;
|
|
if (strcmp(c, "pwd") == 0) { cmd_pwd(p); return 0; }
|
|
if (strcmp(c, "lpwd") == 0) { cmd_lpwd(p); return 0; }
|
|
if (strcmp(c, "cd") == 0) { if (nargs > 1) cmd_cd(p, args[1]); return 0; }
|
|
if (strcmp(c, "lcd") == 0) { if (nargs > 1) cmd_lcd(p, args[1]); return 0; }
|
|
if (strcmp(c, "ls") == 0) { cmd_ls(p, nargs > 1 ? args[1] : NULL); return 0; }
|
|
if (strcmp(c, "lls") == 0) { cmd_lls(p, nargs > 1 ? args[1] : NULL); return 0; }
|
|
if (strcmp(c, "get") == 0) {
|
|
int rec = 0;
|
|
int i = 1;
|
|
if (nargs > 1 && strcmp(args[1], "-r") == 0) { rec = 1; i++; }
|
|
if (nargs > i) cmd_get(p, rec, args[i], nargs > i + 1 ? args[i + 1] : NULL);
|
|
return 0;
|
|
}
|
|
if (strcmp(c, "put") == 0) {
|
|
int rec = 0;
|
|
int i = 1;
|
|
if (nargs > 1 && strcmp(args[1], "-r") == 0) { rec = 1; i++; }
|
|
if (nargs > i) cmd_put(p, rec, args[i], nargs > i + 1 ? args[i + 1] : NULL);
|
|
return 0;
|
|
}
|
|
if (strcmp(c, "rm") == 0) { if (nargs > 1) cmd_rm(p, args[1]); return 0; }
|
|
if (strcmp(c, "mkdir") == 0) { if (nargs > 1) cmd_mkdir(p, args[1]); return 0; }
|
|
if (strcmp(c, "rmdir") == 0) { if (nargs > 1) cmd_rmdir(p, args[1]); return 0; }
|
|
if (strcmp(c, "rename") == 0) { if (nargs > 2) cmd_rename(p, args[1], args[2]); return 0; }
|
|
if (strcmp(c, "help") == 0 || strcmp(c, "?") == 0) { help(); return 0; }
|
|
fprintf(stderr, "psftp: 未知命令 \"%s\" (输入 help 查看)\n", c);
|
|
return 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 入口
|
|
* ============================================================ */
|
|
static void usage(void) {
|
|
fprintf(stderr,
|
|
"psftp - SFTP 客户端 (与 OpenSSH sftp 兼容)\n"
|
|
"用法:\n"
|
|
" psftp [选项] [user@]host[:初始路径]\n"
|
|
"选项:\n"
|
|
" -P <port> 远程端口 (默认 22)\n"
|
|
" -pwd <pw> 密码\n"
|
|
" -i <key> 私钥文件 (publickey 认证)\n"
|
|
" -o <opt> StrictHostKeyChecking=no|accept-new|yes\n"
|
|
" -b <file> 批处理模式:逐行执行命令后退出\n"
|
|
"示例:\n"
|
|
" psftp -P 6666 -pwd pass root@host\n"
|
|
" psftp root@host:/var/log -b batch.txt\n");
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
#ifdef _WIN32
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
#endif
|
|
cli_cfg_t cfg = { 22, NULL, NULL, NULL, 0 };
|
|
const char *batch = NULL;
|
|
const char *target = NULL;
|
|
int i = 1;
|
|
for (; i < argc; i++) {
|
|
const char *a = argv[i];
|
|
if (strcmp(a, "-P") == 0 && i + 1 < argc) { cfg.port = atoi(argv[++i]); }
|
|
else if (strncmp(a, "-P", 2) == 0 && strlen(a) > 2) { cfg.port = atoi(a + 2); }
|
|
else if (strcmp(a, "-pwd") == 0 && i + 1 < argc) { cfg.password_arg = argv[++i]; }
|
|
else if (strncmp(a, "-pwd=", 5) == 0) { cfg.password_arg = a + 5; }
|
|
else if (strcmp(a, "-i") == 0 && i + 1 < argc) { cfg.keyfile = argv[++i]; }
|
|
else if (strcmp(a, "-o") == 0 && i + 1 < argc) {
|
|
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) cfg.hostkey_mode = 2;
|
|
else if (strcmp(v, "accept-new") == 0) cfg.hostkey_mode = 1;
|
|
}
|
|
} else if (strcmp(a, "-b") == 0 && i + 1 < argc) { batch = argv[++i]; }
|
|
else if (a[0] == '-' && strlen(a) > 1) { continue; }
|
|
else { target = a; }
|
|
}
|
|
|
|
if (!target) { usage(); return 1; }
|
|
|
|
/* 拆分 [user@]host[:path] */
|
|
char host[512], ruser[128], start_path[1024];
|
|
const char *colon = strchr(target, ':');
|
|
const char *at = strchr(target, '@');
|
|
if (colon) {
|
|
size_t hl = (size_t)(colon - target);
|
|
if (at && at < colon) {
|
|
size_t ul = (size_t)(at - target);
|
|
if (ul < sizeof(ruser)) { memcpy(ruser, target, ul); ruser[ul] = '\0'; }
|
|
hl = (size_t)(colon - (at + 1));
|
|
if (hl < sizeof(host)) { memcpy(host, at + 1, hl); host[hl] = '\0'; }
|
|
} else {
|
|
if (hl < sizeof(host)) { memcpy(host, target, hl); host[hl] = '\0'; }
|
|
}
|
|
snprintf(start_path, sizeof(start_path), "%s", colon + 1);
|
|
} else {
|
|
size_t tl = strlen(target);
|
|
size_t hl = tl;
|
|
if (at && (size_t)(at - target) < tl) {
|
|
size_t ul = (size_t)(at - target);
|
|
if (ul < sizeof(ruser)) { memcpy(ruser, target, ul); ruser[ul] = '\0'; }
|
|
hl = tl - ul - 1;
|
|
if (hl < sizeof(host)) { memcpy(host, at + 1, hl); host[hl] = '\0'; }
|
|
} else {
|
|
if (hl < sizeof(host)) { memcpy(host, target, hl); host[hl] = '\0'; }
|
|
}
|
|
start_path[0] = '\0';
|
|
}
|
|
|
|
ssh_session_t *s = connect_session(&cfg, host, ruser[0] ? ruser : NULL);
|
|
if (!s) return 1;
|
|
|
|
uint32_t ch = 0;
|
|
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
|
|
fprintf(stderr, "psftp: 通道打开失败\n");
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
ssh_sftp_t *sf = NULL;
|
|
if (ssh_sftp_start(&sf, s, ch) != 0) {
|
|
fprintf(stderr, "psftp: 远端不支持 SFTP 子系统\n");
|
|
ssh_channel_close(s, ch);
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
|
|
psftp_t p = { sf, ".", "" };
|
|
cmd_lpwd(&p);
|
|
if (start_path[0]) cmd_cd(&p, start_path);
|
|
else cmd_pwd(&p);
|
|
|
|
int rc = 0;
|
|
if (batch) {
|
|
FILE *bf = fopen(batch, "r");
|
|
if (!bf) { fprintf(stderr, "psftp: 无法打开批处理文件 %s\n", batch); rc = 1; }
|
|
else {
|
|
char line[4096];
|
|
int first = 1;
|
|
while (fgets(line, sizeof(line), bf)) {
|
|
if (first) {
|
|
first = 0;
|
|
/* 跳过 UTF-8 BOM(EF BB BF),否则首行命令会带上 BOM 无法识别 */
|
|
uint8_t *lp = (uint8_t *)line;
|
|
if (lp[0] == 0xEF && lp[1] == 0xBB && lp[2] == 0xBF)
|
|
memmove(line, line + 3, strlen(line + 3) + 1);
|
|
}
|
|
if (run_line(&p, line)) break;
|
|
}
|
|
fclose(bf);
|
|
}
|
|
} else {
|
|
char line[4096];
|
|
for (;;) {
|
|
fprintf(stderr, "psftp> ");
|
|
fflush(stderr);
|
|
if (!fgets(line, sizeof(line), stdin)) break;
|
|
if (run_line(&p, line)) break;
|
|
}
|
|
}
|
|
|
|
ssh_sftp_free(sf);
|
|
ssh_channel_close(s, ch);
|
|
ssh_session_free(s);
|
|
return rc;
|
|
}
|