# psv-ssh **Repository Path**: rymaker/psv-ssh ## Basic Information - **Project Name**: psv-ssh - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-04 - **Last Updated**: 2026-08-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # VitaSSH+ > PlayStation Vita 原生 SSH / SFTP 客户端 > > 目标平台:PS Vita 3.60+、HENkaku/Ensō、VitaSDK 本项目在 [rompelhd/VitaSSH](https://github.com/rompelhd/VitaSSH) 的结构基础上重做, 按 `../VitaSSH/README.md` 的设计基线实现。上游目录只作只读参考,本项目不修改它。 ## 当前状态 已实现并通过交叉编译(`build/vitassh_plus.vpk`,约 4.0 MiB)与 645 项主机侧单元测试。 **尚未在 Vita3K 或 PSV 真机上验证** —— 见文末"待验证项"。 ## 相对上游的关键改动 | 上游问题 | 本项目的做法 | | --- | --- | | 网络初始化在 UI 线程轮询,最多卡 10 秒 | 全部移入 ConnectionWorker 线程,UI 只消费事件 | | TCP connect / handshake / 认证全部阻塞 | `SCE_NET_SO_NBIO` + libssh2 非阻塞会话,EAGAIN 驱动 | | 只接受点分 IPv4 | `sceNetResolver` 域名解析,带超时与 1 秒粒度的取消 | | 没有任何主机密钥校验 | 认证前强制读取密钥,SHA-256 指纹确认 + `known_hosts`,不匹配直接阻断 | | `profiles.dat` 是裸结构体 fwrite,无版本无校验 | 带版本号的 INI,原子写入,损坏时安全回退;旧文件只迁移非敏感字段 | | 密码明文入盘且无从选择 | 仍是明文(PSV 上没有真加密的可能),但**可以留空**,留空就每次现问 | | 退出只 free 内存块,NetCtl/Net 从未终止 | 按 §8 严格逆序释放,每步幂等,只卸载本应用加载的模块 | | SSH 对象是全局变量,读写有竞争 | 单个 Worker 线程独占 resolver/socket/session/channel/SFTP | | 无统一超时 | 每阶段独立 deadline,错误带阶段、原始错误码和可重试性 | | 单命令输出固定 16 KiB 缓冲 | 64 KiB 环形缓冲 + 丢弃计数,UI 明确显示"输出被截断" | | ANSI 解析每次调用重置,分片序列会漏成正文 | 增量 VT 状态机,转义序列和 UTF-8 都可在任意位置切分 | | IME 结果 `(char)(c & 0xFF)`,非 ASCII 输入损坏 | 完整 UTF-16 ↔ UTF-8 转换(含代理对) | | `VITABUILD` 用 `sha256sums=('SKIP')` | 固定 libssh2 1.11.1 并校验真实 SHA-256,不匹配拒绝构建 | ## 构建 需要 WSL/Linux + VitaSDK。Windows 侧没有 `VITASDK`,构建统一在 WSL 里跑。 ```bash export VITASDK=/usr/local/vitasdk export PATH="$VITASDK/bin:$PATH" ./scripts/build_libssh2.sh # 首次:交叉编译 libssh2 到 third_party/vita-prefix ./scripts/build.sh # 生成 build/vitassh_plus.vpk ./scripts/test.sh # 主机侧单元测试(gcc + ASan/UBSan,不需要 VitaSDK) ``` `scripts/build.sh` 会在缺少 libssh2 时自动先构建依赖。`scripts/gen_assets.py` 生成本项目的 LiveArea 素材。应用内的 VitaShell UI 资源单独放在 `assets/ui/vitashell/`,来源和 GPL-3.0 边界见该目录 README。 代码同时满足严格 ISO C99 和 C11(`-pedantic-errors -Wall -Wextra` 零告警), 实际构建用 `-std=gnu11`。不使用 `strtok_r` / `strnlen` 这类 POSIX 专有函数, 避免依赖特性测试宏在不同工具链上的差异。 ## 安装失败时的排查 VitaShell 安装报错时,先用最小 VPK 定位问题出在哪一层: ```bash cmake -S . -B build-min \ -DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" \ -DVSSH_NO_LIVEAREA=ON cmake --build build-min --parallel "$(nproc)" ``` 最小 VPK 只含 `eboot.bin` + `param.sfo` + `icon0.png`,不含任何 LiveArea 资源。 - **最小能装、完整不能装** → 问题在 LiveArea:检查 `template.xml` 格式, 以及 `bg0.png`(必须 840×500)、`startup.png`(必须 280×158)的尺寸。 - **两个都装不上** → 与本项目的资源无关,检查: - HENkaku 设置里的 **Enable unsafe homebrew** 是否打开 (本项目用 `vita_create_self(... UNSAFE)`,需要它才能运行); - `ux0:` 剩余空间; - 是否已装过同 `TITLE_ID`(`VSSH00001`)的版本,先卸载再装; - 换用 VitaShell 的最新版本重试。 ### 0x8010113D:PNG 必须是 8 位索引色 安装进度走到最后才失败并报 `0x8010113D`,最常见的原因是 **`sce_sys/` 下的 PNG 不是 8 位索引色**。真彩色(PNG color type 2/6, 也就是常说的 24/32 位)会被安装程序拒绝。 要求:**bit depth = 8,color type = 3(indexed),带 PLTE 调色板**。 `file` 命令能一眼看出来: ``` $ file sce_sys/icon0.png sce_sys/icon0.png: PNG image data, 128 x 128, 8-bit colormap, non-interlaced ^^^^^^^^^^^^^^ 必须是这个 ``` 若显示 `8-bit/color RGB` 或 `RGBA` 就是不合规。用 `pngquant` 转换, 或直接跑 `scripts/gen_assets.py`(它在调色板空间里作画, 并把 PLTE 补齐到 256 项 —— 色数太少时某些工具会把位深压到 4 位甚至 2 位,那同样会触发这个错误)。生成后脚本会自行断言格式,不合规直接报错。 ### LiveArea 资源要求(`style="a1"`) | 文件 | 尺寸 | 格式 | | --- | --- | --- | | `sce_sys/icon0.png` | 128×128 | 8 位索引色 PNG | | `livearea/contents/bg0.png` | 840×500 | 8 位索引色 PNG | | `livearea/contents/startup.png` | 280×158 | 8 位索引色 PNG | `template.xml` 里 `` 只能出现在 `` 内部,不能直接挂在 `` 下 —— 这类结构错误同样会让安装阶段的解析失败。 ## 编辑器配置 编辑器报 `Use of undeclared identifier 'g_app'` 这类错误但 gcc 编译正常时, 是 IDE 没找到 `include/` 下的项目头文件,`#include "vssh_app.h"` 整个失败后 连锁报错。仓库里已经配好: - `.clangd` — clangd 用。含相对 include 路径(项目可搬家)和 `armv7a-none-eabi` target。 - `.vscode/c_cpp_properties.json` — C/C++ 扩展用。**两套配置**: 从 WSL Remote 打开选 `PS Vita (WSL)`,从 Windows 侧打开选 `PS Vita (Windows)` (后者通过 `\\wsl.localhost\Ubuntu\...` 访问只装在 WSL 里的 VitaSDK 头文件)。 - `build/compile_commands.json` — 由 `CMAKE_EXPORT_COMPILE_COMMANDS` 生成, 构建一次即可。注意它记录的是 WSL 路径,Windows 侧打开时匹配不上, 这时靠 `.clangd` 里的 fallback 参数兜底。 按 PSV 开发文档的建议,**优先从 WSL Remote 打开项目**,路径和工具链都一致。 ## 架构 ``` 主线程(UI) ConnectionWorker 线程 读输入 / 绘制 960x544 独占 resolver / socket / LIBSSH2_SESSION / channel / SFTP 投递命令 ──── 命令队列 ────► 驱动状态机、deadline、keepalive、退避重连 消费事件 ◄─── 事件队列 ──── 处理短读短写、EAGAIN、EOF、取消 读终端 ◄─── 64KiB 环形 ─── 收到的字节复制进环形缓冲,不碰 UI 数据结构 ``` - 命令队列满时,控制命令(取消/断开/退出)可以挤掉普通命令 —— 保证用户永远能取消。 - 事件队列里 `TERMINAL_DATA` 和 `TRANSFER_PROGRESS` 会合并,状态和错误事件不会被丢。 - 主机密钥确认和 keyboard-interactive 回答走独立的条件变量通道,因为那时 Worker 正阻塞等待,没法去轮询队列。 ### 源码布局 ``` include/ src/ vssh_config.h vssh_util.c 小工具(纯 C,可主机侧测试) vssh_util.h vssh_ring.c 字节环形缓冲 vssh_ring.h vssh_queue.c 有界命令/事件队列 vssh_queue.h vssh_proto.c 状态、错误分类、可重试性 vssh_proto.h vssh_log.c 日志、轮转、脱敏 vssh_log.h vssh_platform_vita.c 平台抽象(时间/线程/文件/目录) vssh_platform.h vssh_net.c SceNet/NetCtl 生命周期 vssh_net.h vssh_sock.c 非阻塞 TCP + DNS vssh_sock.h vssh_ssh.c libssh2 封装 vssh_ssh.h vssh_hostkeys.c known_hosts vssh_hostkeys.h vssh_sftp.c SFTP + 分步传输 vssh_sftp.h vssh_path.c 路径规范化与安全检查 vssh_path.h vssh_profiles.c 档案持久化与迁移 vssh_profiles.h vssh_conn.c ConnectionWorker(编排层) vssh_conn.h vssh_term.c 终端网格 + VT 解析 vssh_term.h vssh_ui.c 主题/控件/触摸命中/IME vssh_ui.h vssh_font.c 内置半角 ASCII 点阵字体(图集与绘制) vssh_font.h vssh_font_ascii.c 点阵数据(由 scripts/gen_font.py 生成) vssh_input_map.h vssh_input_map.c 触摸坐标换算与手势状态机(纯逻辑) vssh_keymap.h vssh_keymap.c 软键盘键位表与按键翻译(纯逻辑) vssh_keyboard.h vssh_keyboard.c 软键盘覆盖层 vssh_app.h vssh_app.c 页面路由 + 菜单/设置/诊断/日志/关于 vssh_page_profiles.c 档案/编辑/连接/主机密钥/认证提问 vssh_page_terminal.c 终端 + 工具条 + 软键盘 vssh_page_sftp.c SFTP 双栏 + 传输任务 main.c tests/ 主机侧 POSIX 平台实现 + 424 项断言 ``` ## 操作 界面全程可触摸,也全程可用手柄;两套操作并存,不必二选一。 **触摸** | 手势 | 行为 | | --- | --- | | 点击 | 选中并执行(列表行、按钮、软键盘按键、模态框选项) | | 长按 | 上下文操作菜单(档案列表、SFTP 条目、终端) | | 拖动 | 滚动(列表、终端历史) | | 点终端区域 | 弹出软键盘 | | 背触拖动 | 滚终端历史(手不挡屏幕,适合翻长日志) | | 背触点击 | 回到终端底部 | **手柄** | 按键 | 菜单 | 终端 | 软键盘 | SFTP | | --- | --- | --- | --- | --- | | O Circle | 确认 | 打开软键盘 | 按下当前键 | 打开目录 / 传输 | | X Cross | 返回 / 取消 | 返回上一页 | — | 返回上级目录 | | Triangle | 新建档案 | IME 发送整行文字 | — | 操作菜单 | | Square | 更多操作 | 打开软键盘 | 收起键盘 | — | | L / R | 翻页 | 滚屏 | 切换键盘页 | 切换本地/远程栏 | | START | 退出 | 会话菜单 | — | 取消传输 | | SELECT | — | 打开设置 | — | 切换隐藏文件 | ### 软键盘 终端页的输入不再走系统 IME 的全屏弹窗。点一下终端就会从屏幕下沿升起一层 自绘键盘(约 228px 高),上方终端始终可见、可滚,按键即时送达,能逐字符输入 ——这是 vim 插入模式、`sudo` 密码提示这类场景唯一可用的方式。 三页布局,右下角切换:字母 / 符号 / 功能键(F1-F12、方向键、Home/End/PgUp/PgDn、 常用 Ctrl 组合)。Shift / Ctrl / Alt 是粘滞键:点一下亮起作用于下一个字符, 再点一下锁定,第三下熄灭。删除键和方向键支持长按连发。 键盘升起或收起时,终端网格会自动重排成正好铺满可视区的行列数并通知远端, 所以光标永远不会被键盘挡在屏幕外。 中文输入仍然需要系统 IME:键盘功能页上有一个 `IME` 键,会话菜单里也有 "发送文字"入口。 ## 数据目录 ``` ux0:data/vitassh/ ├── config/{settings.ini, profiles.ini} ├── keys/ 私钥(自行放入,不打包进 VPK) ├── known_hosts ├── downloads/ SFTP 默认落地目录 ├── transfers/ ├── logs/{vitassh.log, vitassh.old.log} ├── migration/profiles.v1.backup ├── theme/ │ ├── colors.txt 可选:VitaSSH+ / VitaShell 颜色覆盖 │ ├── font.pgf 可选:主题 PGF 覆盖 │ └── *.png 可选:同名组件资源覆盖 └── font.ttf 最高优先级的可选 CJK 字体 ``` `settings.ini` 保存深浅主题、English / 简体中文 / 繁體中文、终端字号、保活间隔、 隐藏文件和背触摸开关。文件带版本号,修改设置时原子写入;损坏或未来版本不会用 未校验的数据覆盖安全默认值。简体中文由项目内的有限转换表生成,新增界面文字后运行 `powershell -File scripts/check_i18n_map.ps1` 检查覆盖面。 ## 字体与中文界面 普通界面文字统一交给 TTF/PVF/PGF 字体后端渲染,保留灰度抗锯齿和准确字号。 检测到 `ux0:data/vitassh/font.ttf` 时,整段界面文字都会使用该字体;否则使用 VPK 内置的 VitaShell PGF,最后才回退系统 PVF/PGF。这样标题、按钮和中英文 混排不会呈现低分辨率点阵锯齿。 终端里的 ASCII 单独使用内置等宽点阵字体。系统字体和用户自备 TTF 的拉丁字形 宽度不可控,终端若直接使用它们,字符可能溢出相邻格。内置字体的 advance 恒等于 半个字高,保证终端严格对齐,也把每帧上千次 freetype 描字换成纹理绘制。 点阵按字号分档预生成 —— **6x12 / 8x16 / 10x20 / 12x24 四套,运行期一律 1:1 绘制,绝不缩放**。终端行高跟着实际字形高度走,而不是用户设定值;这项量化 只影响终端,不再影响普通界面。 点阵数据在 `src/ui/vssh_font_ascii.c`,由 `scripts/gen_font.py` 从 DejaVu Sans Mono(Bitstream Vera 衍生许可,允许再分发与嵌入)离线生成, 产物已入库,构建不依赖 Python。改字体源只需重跑脚本。 普通界面和终端中的非 ASCII(中日韩等)交给字体后端: - 检测到 `ux0:data/vitassh/font.ttf` → 用它渲染(freetype),界面默认中文; - 否则先尝试 `theme/font.pgf`,再用 VPK 内置 PGF,并按系统语言区分英文、简体和繁体; - 内置字体加载失败才用系统 PGF/PVF,并默认英文。 设置页里可以手动循环切换三种语言,选择会持久化。 主题的每个 PNG 都独立按“`theme/` 同名覆盖 → VPK 内置 → 代码绘制”回退。 `theme/colors.txt` 支持 `BG/PANEL/TEXT/ACCENT/...` 原生键,也兼容 VitaShell 常用的 `TITLE_COLOR`、`PATH_COLOR`、`FOCUS_COLOR`、`SETTINGS_MENU_COLOR`、 `PROGRESS_BAR_COLOR` 等键。仓库内的 Electron `colors.txt` 是参考文件,复制到真机 主题目录后才会启用。 VitaShell UI 字体、图标与对话框资源的来源和使用边界见 `assets/ui/vitashell/README.md`;组件接口见 `doc/vitashell-ui-components.md`。 ## 安全设计 - **密码可以存进档案,存的是明文**,落在 `ux0:data/vitassh/config/profiles.ini`。 PSV 没有等同于现代系统安全硬件的凭据存储:任何"加密"都得把密钥一起打进 VPK, 拆包就能拿到,那只是伪装成安全的混淆。所以这里选择诚实而不是假装 —— 任何能读存储卡的人或程序都能看到这个密码,是否接受由你决定。 档案编辑页的密码栏留空即可恢复"每次连接时询问",栏位上也能直接清除已存密码。 - **私钥口令从不写入任何文件**,永远在连接时现问,认证结束立刻清零。 keyboard-interactive 的回答同理。 - 主机密钥在认证之前校验。首次连接显示 SHA-256 指纹要求确认;已保存的密钥 发生变化时**强制阻断,且不提供"忽略并继续"的选项**。 - 日志不记录密码、私钥内容、keyboard-interactive 回答和终端输出; `vssh_log_redact()` 对可能含凭据的字符串做兜底过滤。 - SFTP 下载先写 `.part`,完整后才原子重命名;中断绝不留下伪装成完整文件的结果。 - 远端文件名一律不信任:拒绝含分隔符、`.`、`..` 和控制字符的目录项, 落地前经过 `vssh_path_sanitize_name()`。 - 覆盖与删除显示完整路径并二次确认,危险操作的默认焦点在"取消"上。 ## 已知限制 - **不支持目录的递归上传/下载**,只处理单个文件。 - 同时只允许一个传输任务(规划文档要求,避免占满 Vita 内存)。 - 第一阶段不支持多个 SSH 会话同时在线。 - 终端不支持组合字符(Unicode combining marks)叠加渲染。 - 复制当前行只是把内容显示出来,PSV 没有可用的系统剪贴板。 - AppMgr 恢复事件会立即清空幽灵输入并让连接层验证/重建传输;工作线程仍保留 墙上时钟跳变(≥8 秒)作为漏报时的第二道待机检测。 - OpenSSL 是 VitaSDK 自带的 1.0.2i(已 EOL)。与现代 OpenSSH 的算法协商结果 必须真机实测确认。 ## 待验证项 编译通过不等于真机可用。以下必须实测: **Vita3K** - [ ] VPK 可安装、可启动,各页面切换与按键正常 - [ ] 无网络启动与退出不崩溃 **PSV 真机** - [ ] Wi-Fi 连接、DNS 解析、TCP 连接与 SSH 握手 - [ ] 与现代 OpenSSH 的算法协商(OpenSSL 1.0.2i 是主要风险点) - [ ] 密码 / 公钥 / keyboard-interactive 三条认证路径的成功、失败与取消 - [ ] 首次主机密钥确认与密钥变更阻断 - [ ] `top` / `vim` / `less` / `tmux` 的 PTY 行为与备用屏切换 - [ ] 大输出下的帧率与内存占用,环形缓冲丢弃计数是否符合预期 - [ ] 待机 1 / 5 / 30 分钟后恢复行为 - [ ] Wi-Fi 断开、路由器重启、服务端重启后的退避重连 - [ ] SFTP 上传下载吞吐、断点续传、取消与空间不足 - [ ] 连续 1 小时交互、连续 4 小时内存不增长 - [ ] 导出日志确认不含密码、私钥内容或终端输出 ## 上游与许可 本项目基于 Rompelhd 的 VitaSSH 项目的结构与思路。上游仓库根目录未包含明确的 LICENSE 文件,**公开分发修改版前必须先确认授权与署名要求**。 LiveArea 素材由 `scripts/gen_assets.py` 自行生成,未复用上游美术资源。 第三方组件版本与校验值见 `third_party/README.md`。 本软件只应用于你拥有或获准管理的服务器。