- tls_handshake12.c: 完整 TLS 1.2 client/server 状态机(ECDHE-RSA/ECDSA/RSA AES-GCM + CHACHA20-POLY1305),支持 1.3 收到 1.2 ServerHello/ClientHello 后回退 - tls_record.c: 修正 TLS 1.2 AEAD-GCM 两处 RFC 偏差: AAD length 字段应为明文长度 ctlen(RFC 5246 6.2.3.3)原为 ct+16; GCMNonce = fixed_iv(4) || explicit_nonce(8)(RFC 5288 3)原顺序相反 - tls_handshake13.c: CV/Finished transcript 处理、1.3 套件记录版本修正、链式证书构造 - x509.c: AttributeTypeAndValue 补 SEQUENCE; validity 2050 年前用 UTCTime - apps/pazessl: s_client/s_server 增加 -tls1_2 - 新增 build.ps1、build/run_tls_test.ps1、build/run_openssl_interop.ps1 验证: 自测 4/4(1.2/1.3 双向 + 回退); OpenSSL 互操作 4/4(s_client/s_server 双向 1.2/1.3)
664 行
23 KiB
C
664 行
23 KiB
C
/* pssh scp —— 文件传输 (平替 scp)
|
|
*
|
|
* 传输方式:优先 SFTP 子系统(OpenSSH 9.x scp 默认走 SFTP);
|
|
* 若服务端不支持 SFTP(无 subsystem sftp),自动回退传统 SCP 协议。
|
|
*
|
|
* 用法:
|
|
* pssh scp [-P port] [-p] [-r] [-l user] [-pwd pass] [-i key] [-o opt]
|
|
* src... [user@]host:path 上传
|
|
* pssh scp [-P port] [-p] [-r] [-l user] [-pwd pass] [-i key] [-o opt]
|
|
* [user@]host:path local_path 下载
|
|
*/
|
|
#include "paze/ssh.h"
|
|
#include "paze/ssh_agent.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.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>
|
|
#include <sys/utime.h>
|
|
#define GETCH _getch
|
|
#define IS_SEP(c) ((c) == '/' || (c) == '\\')
|
|
static int local_mkdir(const char *p) { return _mkdir(p); }
|
|
static void local_settime(const char *path, time_t atime, time_t mtime) {
|
|
struct _utimbuf tb;
|
|
tb.actime = atime; tb.modtime = mtime;
|
|
_utime(path, &tb);
|
|
}
|
|
#else
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <utime.h>
|
|
#define IS_SEP(c) ((c) == '/')
|
|
static int local_mkdir(const char *p) { return mkdir(p, 0755); }
|
|
static void local_settime(const char *path, time_t atime, time_t mtime) {
|
|
struct utimbuf tb;
|
|
tb.actime = atime; tb.modtime = mtime;
|
|
utime(path, &tb);
|
|
}
|
|
#endif
|
|
|
|
/* 是否为 [user@]host:path 形式的远端说明 (排除 Windows 盘符 C:\) */
|
|
static int is_remote_spec(const char *a) {
|
|
if (!a) return 0;
|
|
if (a[0] && a[1] == ':') return 0; /* Windows 盘符 */
|
|
return strchr(a, ':') != NULL;
|
|
}
|
|
|
|
/* 拆分 [user@]host:path → host/path (可带 user) */
|
|
static void split_remote(const char *spec, char *host, size_t hlen,
|
|
char *user, size_t ulen, char *path, size_t plen) {
|
|
const char *colon = strchr(spec, ':');
|
|
const char *at = strchr(spec, '@');
|
|
size_t hostlen = (size_t)(colon - spec);
|
|
if (user && ulen && at && at < colon) {
|
|
size_t ul = (size_t)(at - spec);
|
|
if (ul < ulen) { memcpy(user, spec, ul); user[ul] = '\0'; }
|
|
}
|
|
if (at && at < colon) {
|
|
hostlen = (size_t)(colon - (at + 1));
|
|
if (hostlen < hlen) { memcpy(host, at + 1, hostlen); host[hostlen] = '\0'; }
|
|
} else {
|
|
if (hostlen < hlen) { memcpy(host, spec, hostlen); host[hostlen] = '\0'; }
|
|
}
|
|
if (path && plen) {
|
|
const char *p = colon + 1;
|
|
size_t pl = strlen(p);
|
|
if (pl < plen) { memcpy(path, p, pl); path[pl] = '\0'; }
|
|
}
|
|
}
|
|
|
|
/* 取路径最后一个分隔符后的部分 (不含分隔符) */
|
|
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;
|
|
}
|
|
|
|
/* 路径拼接:本地(跟随系统分隔符) / 远端(始终 '/') */
|
|
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);
|
|
}
|
|
|
|
/* 枚举本地目录(不含 . ..),返回条目数;-1=失败 */
|
|
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;
|
|
}
|
|
|
|
/* ============================================================
|
|
* SFTP 传输
|
|
* ============================================================ */
|
|
#define SFTP_BUF 32768
|
|
|
|
static int sftp_upload_file(ssh_sftp_t *sf, const char *local,
|
|
const char *remote, int preserve) {
|
|
FILE *f = fopen(local, "rb");
|
|
if (!f) { fprintf(stderr, "pssh scp: 无法打开 %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, "pssh scp: 无法在远端创建 %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);
|
|
if (rc == 0 && preserve) {
|
|
struct stat st;
|
|
if (stat(local, &st) == 0)
|
|
ssh_sftp_setstat(sf, remote, (uint32_t)st.st_atime,
|
|
(uint32_t)st.st_mtime);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_download_file(ssh_sftp_t *sf, const char *remote,
|
|
const char *local, uint32_t atime,
|
|
uint32_t mtime, int preserve) {
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_open(sf, remote, SSH_FXF_READ, handle, &hlen) < 0) {
|
|
fprintf(stderr, "pssh scp: 无法在远端打开 %s\n", remote);
|
|
return -1;
|
|
}
|
|
FILE *f = fopen(local, "wb");
|
|
if (!f) {
|
|
fprintf(stderr, "pssh scp: 无法创建 %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, "pssh scp: 读取 %s 失败 (code=%d)\n", remote, code);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
if (r == 0) break; /* EOF */
|
|
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);
|
|
if (rc == 0 && preserve) local_settime(local, atime, mtime);
|
|
return rc;
|
|
}
|
|
|
|
/* 逐级创建远端目录(等价 mkdir -p);已存在则忽略 */
|
|
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_upload_dir(ssh_sftp_t *sf, const char *local,
|
|
const char *remote, int preserve) {
|
|
/* 先确保远端目录存在(递归创建父级) */
|
|
remote_mkdir_p(sf, remote);
|
|
ldir_ent_t *ents = (ldir_ent_t *)malloc(1024 * sizeof(ldir_ent_t));
|
|
if (!ents) { fprintf(stderr, "pssh scp: 内存不足\n"); return -1; }
|
|
int n = local_dir_enum(local, ents, 1024);
|
|
if (n < 0) {
|
|
free(ents);
|
|
fprintf(stderr, "pssh scp: 无法读取目录 %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_upload_dir(sf, lp, rp, preserve) < 0) rc = -1;
|
|
} else if (sftp_upload_file(sf, lp, rp, preserve) < 0) {
|
|
rc = -1;
|
|
}
|
|
}
|
|
free(ents);
|
|
if (rc == 0 && preserve) {
|
|
struct stat st;
|
|
if (stat(local, &st) == 0)
|
|
ssh_sftp_setstat(sf, remote, (uint32_t)st.st_atime,
|
|
(uint32_t)st.st_mtime);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_download_dir(ssh_sftp_t *sf, const char *remote,
|
|
const char *local, int preserve) {
|
|
local_mkdir(local); /* 已存在则忽略错误 */
|
|
uint8_t handle[256];
|
|
size_t hlen = 0;
|
|
if (ssh_sftp_opendir(sf, remote, handle, &hlen) < 0) {
|
|
fprintf(stderr, "pssh scp: 无法打开远端目录 %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_download_dir(sf, rp, lp, preserve) < 0) rc = -1;
|
|
} else if (sftp_download_file(sf, rp, lp, ent.atime, ent.mtime,
|
|
preserve) < 0) {
|
|
rc = -1;
|
|
}
|
|
}
|
|
ssh_sftp_close(sf, handle, hlen);
|
|
if (rc == 0 && preserve) {
|
|
ssh_sftp_ent_t d;
|
|
if (ssh_sftp_stat(sf, remote, &d) == 0)
|
|
local_settime(local, d.atime, d.mtime);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_upload_all(ssh_sftp_t *sf, int nsrc, const char *const *pos,
|
|
const char *rpath, int recursive, int preserve) {
|
|
int target_is_dir = 0;
|
|
size_t rl = strlen(rpath);
|
|
if (rl > 0 && rpath[rl - 1] == '/') target_is_dir = 1;
|
|
ssh_sftp_ent_t ent;
|
|
if (!target_is_dir && ssh_sftp_stat(sf, rpath, &ent) == 0)
|
|
target_is_dir = ((ent.perms & SSH_S_IFMT) == SSH_S_IFDIR);
|
|
if (nsrc > 1) target_is_dir = 1; /* 多源时目标恒为目录 */
|
|
|
|
int rc = 0;
|
|
for (int i = 0; i < nsrc; i++) {
|
|
const char *src = pos[i];
|
|
struct stat lst;
|
|
if (stat(src, &lst) != 0) {
|
|
fprintf(stderr, "pssh scp: 无法访问 %s\n", src);
|
|
rc = 1;
|
|
continue;
|
|
}
|
|
char rp[2048];
|
|
if (target_is_dir)
|
|
rjoin(rp, sizeof(rp), rpath, path_basename(src));
|
|
else
|
|
snprintf(rp, sizeof(rp), "%s", rpath);
|
|
if (S_ISDIR(lst.st_mode)) {
|
|
if (!recursive) {
|
|
fprintf(stderr, "pssh scp: 省略目录 %s (需要 -r)\n", src);
|
|
rc = 1;
|
|
continue;
|
|
}
|
|
if (sftp_upload_dir(sf, src, rp, preserve) < 0) rc = 1;
|
|
} else {
|
|
if (sftp_upload_file(sf, src, rp, preserve) < 0) rc = 1;
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int sftp_download_all(ssh_sftp_t *sf, const char *rsrc,
|
|
const char *target, int recursive, int preserve) {
|
|
ssh_sftp_ent_t ent;
|
|
int is_dir = 0;
|
|
if (ssh_sftp_stat(sf, rsrc, &ent) == 0)
|
|
is_dir = ((ent.perms & SSH_S_IFMT) == SSH_S_IFDIR);
|
|
if (is_dir && !recursive) {
|
|
fprintf(stderr, "pssh scp: 省略目录 %s (需要 -r)\n", rsrc);
|
|
return 1;
|
|
}
|
|
struct stat lst;
|
|
size_t tl = strlen(target);
|
|
int tdir = (tl > 0 && IS_SEP(target[tl - 1])) ||
|
|
(stat(target, &lst) == 0 && S_ISDIR(lst.st_mode));
|
|
char lpath[2048];
|
|
if (is_dir) {
|
|
if (tdir)
|
|
path_join_local(lpath, sizeof(lpath), target, path_basename(rsrc));
|
|
else
|
|
snprintf(lpath, sizeof(lpath), "%s", target);
|
|
return sftp_download_dir(sf, rsrc, lpath, preserve);
|
|
}
|
|
if (tdir)
|
|
path_join_local(lpath, sizeof(lpath), target, path_basename(rsrc));
|
|
else
|
|
snprintf(lpath, sizeof(lpath), "%s", target);
|
|
return sftp_download_file(sf, rsrc, lpath, ent.atime, ent.mtime, preserve);
|
|
}
|
|
|
|
/* ============================================================
|
|
* 传统 SCP 协议回退(服务端无 SFTP 子系统时)
|
|
* ============================================================ */
|
|
static const char *remote_dirname(const char *p, char *out, size_t n) {
|
|
const char *last = strrchr(p, '/');
|
|
if (!last) { snprintf(out, n, "."); return out; }
|
|
size_t len = (size_t)(last - p);
|
|
if (len == 0) { snprintf(out, n, "/"); return out; }
|
|
if (len >= n) len = n - 1;
|
|
memcpy(out, p, len); out[len] = '\0';
|
|
return out;
|
|
}
|
|
static void local_dirname(const char *p, char *out, size_t n) {
|
|
const char *last = NULL;
|
|
size_t len = strlen(p);
|
|
for (size_t i = 0; i < len; i++)
|
|
if (IS_SEP(p[i])) last = p + i;
|
|
if (!last) { snprintf(out, n, "."); return; }
|
|
size_t l = (size_t)(last - p);
|
|
if (l >= n) l = n - 1;
|
|
memcpy(out, p, l); out[l] = '\0';
|
|
}
|
|
static void shell_quote(const char *s, char *out, size_t n) {
|
|
size_t o = 0;
|
|
if (o < n) out[o++] = '\'';
|
|
for (const char *p = s; *p; p++) {
|
|
if (*p == '\'') {
|
|
if (o + 4 < n) { out[o++] = '\''; out[o++] = '\\'; out[o++] = '\''; out[o++] = '\''; }
|
|
} else if (o + 1 < n) {
|
|
out[o++] = *p;
|
|
}
|
|
}
|
|
if (o < n) out[o++] = '\'';
|
|
if (o < n) out[o] = '\0';
|
|
}
|
|
|
|
static int legacy_scp(ssh_session_t *s, uint32_t ch, int target_remote,
|
|
int nsrc, const char *const *pos, const char *target,
|
|
const char *rpath, int recursive, int preserve) {
|
|
int rc = 0;
|
|
char cmd[4096], q[3000];
|
|
if (target_remote) {
|
|
const char *dir_target;
|
|
const char *name_override = NULL;
|
|
size_t rl = strlen(rpath);
|
|
if (rl > 0 && rpath[rl - 1] == '/') {
|
|
dir_target = rpath;
|
|
} else if (nsrc > 1) {
|
|
dir_target = rpath;
|
|
} else {
|
|
static char dpart[1024];
|
|
dir_target = remote_dirname(rpath, dpart, sizeof(dpart));
|
|
name_override = path_basename(rpath);
|
|
}
|
|
shell_quote(dir_target, q, sizeof(q));
|
|
snprintf(cmd, sizeof(cmd), "scp %s%s-t %s",
|
|
recursive ? "-r " : "", preserve ? "-p " : "", q);
|
|
ssh_channel_request_exec(s, ch, cmd);
|
|
for (int i = 0; i < nsrc; i++) {
|
|
const char *dn = (nsrc == 1 && name_override) ? name_override
|
|
: path_basename(pos[i]);
|
|
if (ssh_scp_upload(s, ch, pos[i], dn, recursive, preserve) < 0) rc = 1;
|
|
}
|
|
} else {
|
|
shell_quote(rpath, q, sizeof(q));
|
|
snprintf(cmd, sizeof(cmd), "scp %s%s-f %s",
|
|
recursive ? "-r " : "", preserve ? "-p " : "", q);
|
|
ssh_channel_request_exec(s, ch, cmd);
|
|
struct stat st;
|
|
size_t tl = strlen(target);
|
|
int tdir = (tl > 0 && IS_SEP(target[tl - 1])) ||
|
|
(stat(target, &st) == 0 && S_ISDIR(st.st_mode));
|
|
if (tdir) {
|
|
rc = ssh_scp_download(s, ch, target, NULL, preserve);
|
|
} else {
|
|
char ldir[1024];
|
|
local_dirname(target, ldir, sizeof(ldir));
|
|
rc = ssh_scp_download(s, ch, ldir, path_basename(target), preserve);
|
|
}
|
|
if (rc < 0) rc = 1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 入口
|
|
* ============================================================ */
|
|
static void usage(void) {
|
|
fprintf(stderr,
|
|
"pssh scp - 文件传输 (SFTP 子系统优先, 兼容 OpenSSH 9.x; 服务端不支持时回退 SCP)\n"
|
|
"用法:\n"
|
|
" pssh scp [选项] src... [user@]host:path 上传\n"
|
|
" pssh scp [选项] [user@]host:path local_path 下载\n"
|
|
"选项:\n"
|
|
" -P <port> 远程端口 (默认 22)\n"
|
|
" -p 保留时间戳\n"
|
|
" -r 递归复制目录\n"
|
|
" -l <user> 远程用户名\n"
|
|
" -pwd <pw> 密码\n"
|
|
" -i <key> 私钥文件 (publickey 认证)\n"
|
|
" -o <opt> StrictHostKeyChecking=no|accept-new|yes\n"
|
|
"示例:\n"
|
|
" pssh scp -P 6666 -pwd pass file.txt root@host:/tmp/\n"
|
|
" pssh scp -P 6666 -pwd pass -r dir root@host:/srv/\n"
|
|
" pssh scp -P 6666 -pwd pass root@host:/var/log/x.log .\n");
|
|
}
|
|
|
|
int pssh_cmd_scp(int argc, char **argv) {
|
|
int port = 22, recursive = 0, preserve = 0;
|
|
const char *user = NULL, *password_arg = NULL, *keyfile = NULL;
|
|
int hostkey_mode = 0;
|
|
|
|
const char *pos[64];
|
|
int npos = 0;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
const char *a = argv[i];
|
|
if (strcmp(a, "-P") == 0 && i + 1 < argc) { port = atoi(argv[++i]); }
|
|
else if (strncmp(a, "-P", 2) == 0 && strlen(a) > 2) { port = atoi(a + 2); }
|
|
else if (strcmp(a, "-p") == 0) { preserve = 1; }
|
|
else if (strcmp(a, "-r") == 0) { recursive = 1; }
|
|
else if (strcmp(a, "-l") == 0 && i + 1 < argc) { user = argv[++i]; }
|
|
else if (strcmp(a, "-pwd") == 0 && i + 1 < argc) { password_arg = argv[++i]; }
|
|
else if (strncmp(a, "-pwd=", 5) == 0) { password_arg = a + 5; }
|
|
else if (strcmp(a, "-i") == 0 && i + 1 < argc) { 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) hostkey_mode = 2;
|
|
else if (strcmp(v, "accept-new") == 0) hostkey_mode = 1;
|
|
else hostkey_mode = 0;
|
|
}
|
|
} else if (a[0] == '-' && strlen(a) > 1) {
|
|
continue;
|
|
} else if (npos < 64) {
|
|
pos[npos++] = a;
|
|
}
|
|
}
|
|
|
|
if (npos == 0) { usage(); return 1; }
|
|
const char *target = pos[npos - 1];
|
|
int nsrc = npos - 1;
|
|
|
|
/* 目标主机/路径 */
|
|
static char host[512], rpath[2048], ruser[128];
|
|
int target_remote = 0;
|
|
if (is_remote_spec(target)) {
|
|
target_remote = 1;
|
|
split_remote(target, host, sizeof(host), ruser, sizeof(ruser), rpath, sizeof(rpath));
|
|
} else if (npos == 1) {
|
|
fprintf(stderr, "pssh scp: 需要远端路径 (user@host:path)\n");
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
if (target_remote && nsrc > 0) {
|
|
/* ---- 上传:源必须全部为本地 ---- */
|
|
for (int i = 0; i < nsrc; i++) {
|
|
if (is_remote_spec(pos[i])) {
|
|
fprintf(stderr, "pssh scp: 不支持远程到远程复制\n");
|
|
return 1;
|
|
}
|
|
}
|
|
} else if (!target_remote) {
|
|
/* ---- 下载:源必须恰好一个且为远端 ---- */
|
|
if (nsrc != 1 || !is_remote_spec(pos[0])) {
|
|
fprintf(stderr, "pssh scp: 下载需要一个远程源 (user@host:path) 和一个本地目标\n");
|
|
usage();
|
|
return 1;
|
|
}
|
|
split_remote(pos[0], host, sizeof(host), ruser, sizeof(ruser), rpath, sizeof(rpath));
|
|
}
|
|
|
|
if (ruser[0] && !user) user = ruser;
|
|
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) {
|
|
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, "pssh scp: 会话分配失败\n"); return 1; }
|
|
ssh_session_set_hostkey_check(s, hostkey_mode);
|
|
if (ssh_client_connect(s, host, (uint16_t)port) < 0) {
|
|
fprintf(stderr, "pssh scp: 连接失败\n");
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
|
|
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 scp: 警告: 无法读取密钥文件 %s\n", keyfile);
|
|
}
|
|
|
|
int auth_ok = 0;
|
|
if (keyfile) {
|
|
auth_ok = ssh_auth_client_loop(s, user, password, keydata, keylen) == 0;
|
|
} 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;
|
|
}
|
|
free(keydata);
|
|
if (!auth_ok) {
|
|
fprintf(stderr, "pssh scp: 认证失败\n");
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
|
|
uint32_t ch = 0;
|
|
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
|
|
fprintf(stderr, "pssh scp: 通道打开失败\n");
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
|
|
int rc = 0;
|
|
ssh_sftp_t *sf = NULL;
|
|
if (ssh_sftp_start(&sf, s, ch) == 0) {
|
|
/* ---- SFTP 优先 ---- */
|
|
if (target_remote) {
|
|
rc = sftp_upload_all(sf, nsrc, (const char *const *)pos,
|
|
rpath, recursive, preserve);
|
|
} else {
|
|
rc = sftp_download_all(sf, rpath, target, recursive, preserve);
|
|
}
|
|
ssh_sftp_free(sf);
|
|
} else {
|
|
/* ---- 服务端不支持 SFTP:回退传统 SCP ---- */
|
|
rc = legacy_scp(s, ch, target_remote, nsrc, (const char *const *)pos,
|
|
target, rpath, recursive, preserve);
|
|
}
|
|
|
|
ssh_channel_close(s, ch);
|
|
ssh_session_free(s);
|
|
return rc;
|
|
}
|