feat: add libssh git ssh gateway demo
This commit is contained in:
commit
e014dca3ca
|
|
@ -0,0 +1,11 @@
|
|||
build/
|
||||
build-*/
|
||||
cmake-build-*/
|
||||
|
||||
ssh_host_*_key
|
||||
ssh_host_*_key.pub
|
||||
|
||||
repositories/
|
||||
repo/
|
||||
*.log
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(git_ssh_gateway C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_EXTENSIONS OFF)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh)
|
||||
|
||||
add_executable(git-ssh-gateway
|
||||
src/git_ssh_gateway.c
|
||||
)
|
||||
|
||||
target_compile_options(git-ssh-gateway PRIVATE
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
)
|
||||
|
||||
target_link_libraries(git-ssh-gateway PRIVATE
|
||||
PkgConfig::LIBSSH
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
# OpenResty stream + libssh Git SSH Gateway Demo
|
||||
|
||||
目标:让 `git clone git@example.com:owner/repo.git` 进入 OpenResty 的 `stream` 入口后,转发到一个基于 libssh 的 SSH gateway,由 gateway 解析 Git SSH 请求并转发到真实 Git 后端。
|
||||
|
||||
这个仓库包含一个可编译的 C/CMake demo:
|
||||
|
||||
```text
|
||||
CMakeLists.txt
|
||||
src/git_ssh_gateway.c
|
||||
openresty/nginx.conf
|
||||
scripts/gen-host-key.sh
|
||||
```
|
||||
|
||||
demo 当前实现的是:
|
||||
|
||||
```text
|
||||
git client
|
||||
|
|
||||
| SSH
|
||||
v
|
||||
OpenResty stream
|
||||
|
|
||||
| TCP
|
||||
v
|
||||
libssh gateway
|
||||
|
|
||||
| fork/exec
|
||||
v
|
||||
local git-upload-pack / git-receive-pack
|
||||
```
|
||||
|
||||
也就是说,libssh gateway 会终止 SSH、解析 Git `exec` 命令,然后在 `--repo-root` 目录下执行本机的 `git-upload-pack`、`git-receive-pack` 或 `git-upload-archive`。
|
||||
|
||||
## 依赖
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
brew install cmake pkg-config libssh
|
||||
```
|
||||
|
||||
Ubuntu/Debian:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential cmake pkg-config libssh-dev git openssh-client
|
||||
```
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
## 准备测试仓库
|
||||
|
||||
```bash
|
||||
mkdir -p repositories/owner
|
||||
git init --bare repositories/owner/repo.git
|
||||
```
|
||||
|
||||
## 生成 SSH host key
|
||||
|
||||
```bash
|
||||
./scripts/gen-host-key.sh ./ssh_host_ed25519_key
|
||||
```
|
||||
|
||||
## 启动 gateway
|
||||
|
||||
```bash
|
||||
./build/git-ssh-gateway \
|
||||
--bind 127.0.0.1 \
|
||||
--port 2222 \
|
||||
--repo-root ./repositories \
|
||||
--host-key ./ssh_host_ed25519_key
|
||||
```
|
||||
|
||||
直接连 gateway 测试:
|
||||
|
||||
```bash
|
||||
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222" \
|
||||
git clone git@127.0.0.1:owner/repo.git
|
||||
```
|
||||
|
||||
经过 OpenResty 测试:
|
||||
|
||||
```bash
|
||||
openresty -p "$PWD" -c openresty/nginx.conf
|
||||
|
||||
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2220" \
|
||||
git clone git@127.0.0.1:owner/repo.git
|
||||
```
|
||||
|
||||
本 demo 为方便本地验证,认证层接受任意 publickey/password。生产环境必须改成真实的 key 校验、用户映射、仓库 ACL 和审计。
|
||||
|
||||
## 结论
|
||||
|
||||
如果只是透明 TCP 转发,OpenResty `stream` 已经足够,不需要 libssh:
|
||||
|
||||
```nginx
|
||||
stream {
|
||||
upstream git_ssh_backend {
|
||||
server 127.0.0.1:2222;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 22;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_timeout 1h;
|
||||
proxy_pass git_ssh_backend;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要按仓库、用户、租户、权限、审计或灰度路由,就必须终止 SSH。原因是 Git SSH 请求在 SSH 加密通道内,OpenResty `stream` 的 preread 阶段最多只能看到 SSH banner,例如:
|
||||
|
||||
```text
|
||||
SSH-2.0-OpenSSH_9.x
|
||||
```
|
||||
|
||||
它看不到后续的:
|
||||
|
||||
```text
|
||||
git-upload-pack 'owner/repo.git'
|
||||
git-receive-pack 'owner/repo.git'
|
||||
git-upload-archive 'owner/repo.git'
|
||||
```
|
||||
|
||||
所以推荐架构是:
|
||||
|
||||
```text
|
||||
git client
|
||||
|
|
||||
| TCP/22
|
||||
v
|
||||
OpenResty stream
|
||||
|
|
||||
| TCP/2222
|
||||
v
|
||||
libssh git gateway
|
||||
|
|
||||
| SSH or local git-shell
|
||||
v
|
||||
real git backend
|
||||
```
|
||||
|
||||
## OpenResty 配置
|
||||
|
||||
`openresty/nginx.conf`:
|
||||
|
||||
```nginx
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
stream {
|
||||
log_format ssh_log '$remote_addr [$time_local] '
|
||||
'$protocol $status $bytes_sent $bytes_received '
|
||||
'$session_time';
|
||||
|
||||
access_log logs/git-ssh-access.log ssh_log;
|
||||
error_log logs/git-ssh-error.log info;
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=ssh_ip:10m;
|
||||
|
||||
upstream libssh_git_gateway {
|
||||
server 127.0.0.1:2222 max_fails=3 fail_timeout=10s;
|
||||
keepalive 128;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 22 reuseport;
|
||||
|
||||
proxy_connect_timeout 3s;
|
||||
proxy_timeout 1h;
|
||||
proxy_socket_keepalive on;
|
||||
|
||||
limit_conn ssh_ip 20;
|
||||
|
||||
proxy_pass libssh_git_gateway;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## libssh gateway 要做什么
|
||||
|
||||
gateway 是一个 SSH server,不是普通 TCP proxy。核心流程:
|
||||
|
||||
1. `ssh_bind_new` 监听 `127.0.0.1:2222`。
|
||||
2. 加载 host key,让客户端信任的是 gateway。
|
||||
3. 完成 SSH key exchange。
|
||||
4. 处理认证:publickey/password/token 均可,生产环境建议 publickey。
|
||||
5. 接收 `session` channel。
|
||||
6. 读取 `exec` 请求,解析命令:
|
||||
|
||||
```text
|
||||
git-upload-pack 'owner/repo.git'
|
||||
git-receive-pack 'owner/repo.git'
|
||||
git-upload-archive 'owner/repo.git'
|
||||
```
|
||||
|
||||
7. 做权限校验和仓库路由。
|
||||
8. 转发到真实后端,二选一:
|
||||
|
||||
- 后端也是 SSH:gateway 再作为 libssh client 连接后端,发同样的 `exec` 命令,然后双向转发 channel。
|
||||
- 后端是本机仓库:直接 fork/exec `git-upload-pack` 或 `git-receive-pack`,把 SSH channel stdin/stdout/stderr 接到子进程。
|
||||
|
||||
## Git SSH 命令解析规则
|
||||
|
||||
只允许 Git 白名单命令:
|
||||
|
||||
```text
|
||||
git-upload-pack
|
||||
git-receive-pack
|
||||
git-upload-archive
|
||||
```
|
||||
|
||||
仓库路径必须规范化,避免命令注入和路径穿越:
|
||||
|
||||
```text
|
||||
owner/repo.git
|
||||
/owner/repo.git
|
||||
'owner/repo.git'
|
||||
"owner/repo.git"
|
||||
```
|
||||
|
||||
建议解析后统一成:
|
||||
|
||||
```text
|
||||
owner/repo.git
|
||||
```
|
||||
|
||||
拒绝这些输入:
|
||||
|
||||
```text
|
||||
../../etc/passwd
|
||||
owner/repo.git; id
|
||||
owner/repo.git && id
|
||||
owner/../repo.git
|
||||
```
|
||||
|
||||
## 推荐的 gateway 伪代码
|
||||
|
||||
```c
|
||||
ssh_bind bind = ssh_bind_new();
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_BINDADDR, "127.0.0.1");
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_BINDPORT_STR, "2222");
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_RSAKEY, "/etc/git-gateway/ssh_host_rsa_key");
|
||||
ssh_bind_listen(bind);
|
||||
|
||||
for (;;) {
|
||||
ssh_session client = ssh_new();
|
||||
ssh_bind_accept(bind, client);
|
||||
|
||||
if (ssh_handle_key_exchange(client) != SSH_OK) {
|
||||
ssh_disconnect(client);
|
||||
ssh_free(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
authenticate_client_publickey(client);
|
||||
|
||||
ssh_channel chan = accept_session_channel(client);
|
||||
char *exec = read_exec_request(client);
|
||||
|
||||
struct git_request req = parse_git_exec(exec);
|
||||
authorize(req.user, req.repo, req.operation);
|
||||
|
||||
ssh_session backend = connect_backend(req);
|
||||
ssh_channel backend_chan = open_backend_exec(backend, exec);
|
||||
|
||||
pump_bidirectional(chan, backend_chan);
|
||||
|
||||
ssh_channel_close(backend_chan);
|
||||
ssh_channel_close(chan);
|
||||
ssh_disconnect(backend);
|
||||
ssh_disconnect(client);
|
||||
}
|
||||
```
|
||||
|
||||
## 双向转发注意事项
|
||||
|
||||
SSH channel 不是裸 socket,不能直接 `splice`。需要循环读取两边 channel:
|
||||
|
||||
```text
|
||||
client channel stdout/stderr <-> backend channel stdout/stderr
|
||||
client channel stdin <-> backend channel stdin
|
||||
```
|
||||
|
||||
需要处理:
|
||||
|
||||
- EOF:一侧 `ssh_channel_send_eof` 后继续读另一侧剩余数据。
|
||||
- stderr:Git 会通过 sideband 和 stderr 传进度信息,不能吞掉。
|
||||
- exit status:后端 exit status 要回传给客户端。
|
||||
- backpressure:不要一次性读入内存,按 16KB 或 32KB chunk 转发。
|
||||
- 超时:clone 大仓库可能很久,读写超时要比 HTTP 长。
|
||||
|
||||
## 为什么不把 libssh 直接塞进 OpenResty Lua
|
||||
|
||||
不推荐。OpenResty `stream_lua` 可以处理 TCP,但 libssh 是完整 SSH 协议栈,包含握手、认证、channel、窗口、加密状态机。把它同步阻塞地放进 nginx worker 会卡 worker;写成 nginx C 模块又要处理事件模型、内存池、生命周期和非阻塞 libssh,复杂度远高于独立 gateway。
|
||||
|
||||
更稳的边界是:
|
||||
|
||||
```text
|
||||
OpenResty = 四层入口、限流、日志、连接保护
|
||||
libssh gateway = SSH 协议终止、认证、仓库路由、审计
|
||||
Git backend = 实际仓库读写
|
||||
```
|
||||
|
||||
## 测试命令
|
||||
|
||||
本地调试 gateway:
|
||||
|
||||
```bash
|
||||
ssh -vvv -p 2222 git@127.0.0.1 "git-upload-pack 'owner/repo.git'"
|
||||
```
|
||||
|
||||
经过 OpenResty:
|
||||
|
||||
```bash
|
||||
GIT_SSH_COMMAND="ssh -vvv -p 22" git clone git@127.0.0.1:owner/repo.git
|
||||
```
|
||||
|
||||
如果客户端报 host key changed,说明现在 SSH 终止点换成了 gateway,需要更新 known_hosts。
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
stream {
|
||||
log_format ssh_log '$remote_addr [$time_local] '
|
||||
'$protocol $status $bytes_sent $bytes_received '
|
||||
'$session_time';
|
||||
|
||||
access_log logs/git-ssh-access.log ssh_log;
|
||||
error_log logs/git-ssh-error.log info;
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=ssh_ip:10m;
|
||||
|
||||
upstream libssh_git_gateway {
|
||||
server 127.0.0.1:2222 max_fails=3 fail_timeout=10s;
|
||||
}
|
||||
|
||||
server {
|
||||
# Use 2220 for unprivileged local testing. Change to 22 in production.
|
||||
listen 2220 reuseport;
|
||||
|
||||
proxy_connect_timeout 3s;
|
||||
proxy_timeout 1h;
|
||||
proxy_socket_keepalive on;
|
||||
|
||||
limit_conn ssh_ip 20;
|
||||
|
||||
proxy_pass libssh_git_gateway;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
key_path="${1:-./ssh_host_ed25519_key}"
|
||||
|
||||
if [ -f "$key_path" ]; then
|
||||
echo "host key already exists: $key_path"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh-keygen -t ed25519 -N "" -f "$key_path"
|
||||
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
#include <libssh/server.h>
|
||||
|
||||
#define DEFAULT_BIND_ADDR "127.0.0.1"
|
||||
#define DEFAULT_BIND_PORT "2222"
|
||||
#define DEFAULT_REPO_ROOT "./repositories"
|
||||
#define DEFAULT_HOST_KEY "./ssh_host_ed25519_key"
|
||||
#define IO_BUF_SIZE 32768
|
||||
|
||||
struct config {
|
||||
const char *bind_addr;
|
||||
const char *bind_port;
|
||||
const char *repo_root;
|
||||
const char *host_key;
|
||||
};
|
||||
|
||||
struct git_request {
|
||||
const char *program;
|
||||
char repo[1024];
|
||||
};
|
||||
|
||||
struct child_proc {
|
||||
pid_t pid;
|
||||
int stdin_fd;
|
||||
int stdout_fd;
|
||||
int stderr_fd;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t g_stop = 0;
|
||||
|
||||
static void on_signal(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
g_stop = 1;
|
||||
}
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Usage: %s [--bind ADDR] [--port PORT] [--repo-root DIR] [--host-key FILE]\n"
|
||||
"\n"
|
||||
"Example:\n"
|
||||
" %s --bind 127.0.0.1 --port 2222 --repo-root ./repositories --host-key ./ssh_host_ed25519_key\n",
|
||||
argv0, argv0);
|
||||
}
|
||||
|
||||
static struct config parse_args(int argc, char **argv)
|
||||
{
|
||||
struct config cfg = {
|
||||
.bind_addr = DEFAULT_BIND_ADDR,
|
||||
.bind_port = DEFAULT_BIND_PORT,
|
||||
.repo_root = DEFAULT_REPO_ROOT,
|
||||
.host_key = DEFAULT_HOST_KEY,
|
||||
};
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--bind") == 0 && i + 1 < argc) {
|
||||
cfg.bind_addr = argv[++i];
|
||||
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||
cfg.bind_port = argv[++i];
|
||||
} else if (strcmp(argv[i], "--repo-root") == 0 && i + 1 < argc) {
|
||||
cfg.repo_root = argv[++i];
|
||||
} else if (strcmp(argv[i], "--host-key") == 0 && i + 1 < argc) {
|
||||
cfg.host_key = argv[++i];
|
||||
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
usage(argv[0]);
|
||||
exit(0);
|
||||
} else {
|
||||
usage(argv[0]);
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static int set_nonblock(int fd)
|
||||
{
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
return -1;
|
||||
}
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
static void close_if_open(int *fd)
|
||||
{
|
||||
if (*fd >= 0) {
|
||||
close(*fd);
|
||||
*fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool has_bad_repo_chars(const char *s)
|
||||
{
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
if (*p <= 0x20 || *p == 0x7f) {
|
||||
return true;
|
||||
}
|
||||
switch (*p) {
|
||||
case ';':
|
||||
case '&':
|
||||
case '|':
|
||||
case '`':
|
||||
case '$':
|
||||
case '<':
|
||||
case '>':
|
||||
case '\\':
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int parse_git_exec(const char *command, struct git_request *req)
|
||||
{
|
||||
const char *programs[] = {
|
||||
"git-upload-pack",
|
||||
"git-receive-pack",
|
||||
"git-upload-archive",
|
||||
};
|
||||
|
||||
memset(req, 0, sizeof(*req));
|
||||
|
||||
for (size_t i = 0; i < sizeof(programs) / sizeof(programs[0]); i++) {
|
||||
size_t len = strlen(programs[i]);
|
||||
if (strncmp(command, programs[i], len) == 0 &&
|
||||
(command[len] == ' ' || command[len] == '\t')) {
|
||||
req->program = programs[i];
|
||||
command += len;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (req->program == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (*command == ' ' || *command == '\t') {
|
||||
command++;
|
||||
}
|
||||
|
||||
char quote = 0;
|
||||
if (*command == '\'' || *command == '"') {
|
||||
quote = *command++;
|
||||
}
|
||||
|
||||
size_t n = 0;
|
||||
while (*command) {
|
||||
if (quote != 0) {
|
||||
if (*command == quote) {
|
||||
command++;
|
||||
break;
|
||||
}
|
||||
} else if (*command == ' ' || *command == '\t') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (n + 1 >= sizeof(req->repo)) {
|
||||
return -1;
|
||||
}
|
||||
req->repo[n++] = *command++;
|
||||
}
|
||||
req->repo[n] = '\0';
|
||||
|
||||
while (*command == ' ' || *command == '\t') {
|
||||
command++;
|
||||
}
|
||||
if (*command != '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (req->repo[0] == '/') {
|
||||
memmove(req->repo, req->repo + 1, strlen(req->repo));
|
||||
}
|
||||
|
||||
if (req->repo[0] == '\0' || strstr(req->repo, "..") != NULL ||
|
||||
has_bad_repo_chars(req->repo)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int authenticate_client(ssh_session session)
|
||||
{
|
||||
ssh_message msg;
|
||||
|
||||
ssh_set_auth_methods(session, SSH_AUTH_METHOD_PUBLICKEY | SSH_AUTH_METHOD_PASSWORD);
|
||||
|
||||
while ((msg = ssh_message_get(session)) != NULL) {
|
||||
if (ssh_message_type(msg) == SSH_REQUEST_AUTH) {
|
||||
int subtype = ssh_message_subtype(msg);
|
||||
const char *user = ssh_message_auth_user(msg);
|
||||
|
||||
if (subtype == SSH_AUTH_METHOD_PUBLICKEY) {
|
||||
if (ssh_message_auth_publickey_state(msg) == SSH_PUBLICKEY_STATE_NONE) {
|
||||
ssh_message_auth_reply_pk_ok_simple(msg);
|
||||
ssh_message_free(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stderr, "accepted publickey auth for user=%s\n", user ? user : "");
|
||||
ssh_message_auth_reply_success(msg, 0);
|
||||
ssh_message_free(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subtype == SSH_AUTH_METHOD_PASSWORD) {
|
||||
fprintf(stderr, "accepted password auth for user=%s\n", user ? user : "");
|
||||
ssh_message_auth_reply_success(msg, 0);
|
||||
ssh_message_free(msg);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ssh_message_reply_default(msg);
|
||||
ssh_message_free(msg);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ssh_channel accept_exec_channel(ssh_session session, char *command, size_t command_len)
|
||||
{
|
||||
ssh_channel channel = NULL;
|
||||
ssh_message msg;
|
||||
|
||||
while ((msg = ssh_message_get(session)) != NULL) {
|
||||
if (ssh_message_type(msg) == SSH_REQUEST_CHANNEL_OPEN &&
|
||||
ssh_message_subtype(msg) == SSH_CHANNEL_SESSION) {
|
||||
channel = ssh_message_channel_request_open_reply_accept(msg);
|
||||
ssh_message_free(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
ssh_message_reply_default(msg);
|
||||
ssh_message_free(msg);
|
||||
}
|
||||
|
||||
if (channel == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while ((msg = ssh_message_get(session)) != NULL) {
|
||||
if (ssh_message_type(msg) == SSH_REQUEST_CHANNEL &&
|
||||
ssh_message_subtype(msg) == SSH_CHANNEL_REQUEST_EXEC) {
|
||||
const char *cmd = ssh_message_channel_request_command(msg);
|
||||
if (cmd == NULL || strlen(cmd) + 1 > command_len) {
|
||||
ssh_message_reply_default(msg);
|
||||
ssh_message_free(msg);
|
||||
ssh_channel_free(channel);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strcpy(command, cmd);
|
||||
ssh_message_channel_request_reply_success(msg);
|
||||
ssh_message_free(msg);
|
||||
return channel;
|
||||
}
|
||||
|
||||
ssh_message_reply_default(msg);
|
||||
ssh_message_free(msg);
|
||||
}
|
||||
|
||||
ssh_channel_free(channel);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int spawn_git_child(const struct config *cfg, const struct git_request *req, struct child_proc *child)
|
||||
{
|
||||
int in_pipe[2] = {-1, -1};
|
||||
int out_pipe[2] = {-1, -1};
|
||||
int err_pipe[2] = {-1, -1};
|
||||
|
||||
child->pid = -1;
|
||||
child->stdin_fd = -1;
|
||||
child->stdout_fd = -1;
|
||||
child->stderr_fd = -1;
|
||||
|
||||
if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0 || pipe(err_pipe) != 0) {
|
||||
perror("pipe");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
dup2(in_pipe[0], STDIN_FILENO);
|
||||
dup2(out_pipe[1], STDOUT_FILENO);
|
||||
dup2(err_pipe[1], STDERR_FILENO);
|
||||
|
||||
close(in_pipe[0]);
|
||||
close(in_pipe[1]);
|
||||
close(out_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
close(err_pipe[0]);
|
||||
close(err_pipe[1]);
|
||||
|
||||
if (chdir(cfg->repo_root) != 0) {
|
||||
perror("chdir repo root");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
execlp(req->program, req->program, req->repo, (char *)NULL);
|
||||
perror("execlp git program");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
close(in_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
close(err_pipe[1]);
|
||||
|
||||
child->pid = pid;
|
||||
child->stdin_fd = in_pipe[1];
|
||||
child->stdout_fd = out_pipe[0];
|
||||
child->stderr_fd = err_pipe[0];
|
||||
|
||||
set_nonblock(child->stdout_fd);
|
||||
set_nonblock(child->stderr_fd);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
close_if_open(&in_pipe[0]);
|
||||
close_if_open(&in_pipe[1]);
|
||||
close_if_open(&out_pipe[0]);
|
||||
close_if_open(&out_pipe[1]);
|
||||
close_if_open(&err_pipe[0]);
|
||||
close_if_open(&err_pipe[1]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int pump_channel_to_child(ssh_channel channel, int *child_stdin)
|
||||
{
|
||||
char buf[IO_BUF_SIZE];
|
||||
|
||||
for (;;) {
|
||||
int n = ssh_channel_read_nonblocking(channel, buf, sizeof(buf), 0);
|
||||
if (n == SSH_ERROR) {
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
ssize_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t wr = write(*child_stdin, buf + off, (size_t)(n - off));
|
||||
if (wr > 0) {
|
||||
off += wr;
|
||||
continue;
|
||||
}
|
||||
if (wr < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
close_if_open(child_stdin);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (ssh_channel_is_eof(channel)) {
|
||||
close_if_open(child_stdin);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pump_fd_to_channel(int *fd, ssh_channel channel, int is_stderr)
|
||||
{
|
||||
char buf[IO_BUF_SIZE];
|
||||
|
||||
for (;;) {
|
||||
ssize_t n = read(*fd, buf, sizeof(buf));
|
||||
if (n > 0) {
|
||||
int rc = is_stderr ? ssh_channel_write_stderr(channel, buf, (uint32_t)n)
|
||||
: ssh_channel_write(channel, buf, (uint32_t)n);
|
||||
if (rc == SSH_ERROR) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (n == 0) {
|
||||
close_if_open(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
close_if_open(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int relay(ssh_session session, ssh_channel channel, struct child_proc *child)
|
||||
{
|
||||
int exit_status = 255;
|
||||
bool child_done = false;
|
||||
|
||||
while (!g_stop) {
|
||||
struct pollfd fds[3];
|
||||
nfds_t nfds = 0;
|
||||
int ssh_fd = ssh_get_fd(session);
|
||||
|
||||
fds[nfds].fd = ssh_fd;
|
||||
fds[nfds].events = POLLIN;
|
||||
nfds++;
|
||||
|
||||
if (child->stdout_fd >= 0) {
|
||||
fds[nfds].fd = child->stdout_fd;
|
||||
fds[nfds].events = POLLIN;
|
||||
nfds++;
|
||||
}
|
||||
|
||||
if (child->stderr_fd >= 0) {
|
||||
fds[nfds].fd = child->stderr_fd;
|
||||
fds[nfds].events = POLLIN;
|
||||
nfds++;
|
||||
}
|
||||
|
||||
int rc = poll(fds, nfds, 100);
|
||||
if (rc < 0 && errno != EINTR) {
|
||||
perror("poll");
|
||||
break;
|
||||
}
|
||||
|
||||
if (child->stdin_fd >= 0 && pump_channel_to_child(channel, &child->stdin_fd) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (child->stdout_fd >= 0 && pump_fd_to_channel(&child->stdout_fd, channel, 0) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (child->stderr_fd >= 0 && pump_fd_to_channel(&child->stderr_fd, channel, 1) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!child_done) {
|
||||
int status = 0;
|
||||
pid_t got = waitpid(child->pid, &status, WNOHANG);
|
||||
if (got == child->pid) {
|
||||
child_done = true;
|
||||
if (WIFEXITED(status)) {
|
||||
exit_status = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
exit_status = 128 + WTERMSIG(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child_done && child->stdout_fd < 0 && child->stderr_fd < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
close_if_open(&child->stdin_fd);
|
||||
close_if_open(&child->stdout_fd);
|
||||
close_if_open(&child->stderr_fd);
|
||||
|
||||
if (!child_done && child->pid > 0) {
|
||||
int status = 0;
|
||||
kill(child->pid, SIGTERM);
|
||||
waitpid(child->pid, &status, 0);
|
||||
}
|
||||
|
||||
ssh_channel_request_send_exit_status(channel, exit_status);
|
||||
ssh_channel_send_eof(channel);
|
||||
return exit_status;
|
||||
}
|
||||
|
||||
static void handle_session(const struct config *cfg, ssh_session session)
|
||||
{
|
||||
char command[2048];
|
||||
struct git_request req;
|
||||
ssh_channel channel = NULL;
|
||||
struct child_proc child;
|
||||
|
||||
memset(command, 0, sizeof(command));
|
||||
|
||||
if (ssh_handle_key_exchange(session) != SSH_OK) {
|
||||
fprintf(stderr, "key exchange failed: %s\n", ssh_get_error(session));
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticate_client(session) != 0) {
|
||||
fprintf(stderr, "authentication failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
channel = accept_exec_channel(session, command, sizeof(command));
|
||||
if (channel == NULL) {
|
||||
fprintf(stderr, "no exec channel\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "exec: %s\n", command);
|
||||
|
||||
if (parse_git_exec(command, &req) != 0) {
|
||||
const char *msg = "unsupported or unsafe git ssh command\n";
|
||||
ssh_channel_write_stderr(channel, msg, (uint32_t)strlen(msg));
|
||||
ssh_channel_request_send_exit_status(channel, 127);
|
||||
ssh_channel_send_eof(channel);
|
||||
ssh_channel_close(channel);
|
||||
ssh_channel_free(channel);
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "git request: program=%s repo=%s\n", req.program, req.repo);
|
||||
|
||||
if (spawn_git_child(cfg, &req, &child) != 0) {
|
||||
const char *msg = "failed to start git backend\n";
|
||||
ssh_channel_write_stderr(channel, msg, (uint32_t)strlen(msg));
|
||||
ssh_channel_request_send_exit_status(channel, 127);
|
||||
} else {
|
||||
relay(session, channel, &child);
|
||||
}
|
||||
|
||||
ssh_channel_close(channel);
|
||||
ssh_channel_free(channel);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct config cfg = parse_args(argc, argv);
|
||||
ssh_bind bind = NULL;
|
||||
|
||||
signal(SIGINT, on_signal);
|
||||
signal(SIGTERM, on_signal);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
bind = ssh_bind_new();
|
||||
if (bind == NULL) {
|
||||
fprintf(stderr, "ssh_bind_new failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_BINDADDR, cfg.bind_addr);
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_BINDPORT_STR, cfg.bind_port);
|
||||
ssh_bind_options_set(bind, SSH_BIND_OPTIONS_HOSTKEY, cfg.host_key);
|
||||
|
||||
if (ssh_bind_listen(bind) != SSH_OK) {
|
||||
fprintf(stderr, "listen failed: %s\n", ssh_get_error(bind));
|
||||
ssh_bind_free(bind);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"git-ssh-gateway listening on %s:%s, repo_root=%s, host_key=%s\n",
|
||||
cfg.bind_addr, cfg.bind_port, cfg.repo_root, cfg.host_key);
|
||||
|
||||
while (!g_stop) {
|
||||
ssh_session session = ssh_new();
|
||||
if (session == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ssh_bind_accept(bind, session) == SSH_ERROR) {
|
||||
if (!g_stop) {
|
||||
fprintf(stderr, "accept failed: %s\n", ssh_get_error(bind));
|
||||
}
|
||||
ssh_free(session);
|
||||
continue;
|
||||
}
|
||||
|
||||
handle_session(&cfg, session);
|
||||
ssh_disconnect(session);
|
||||
ssh_free(session);
|
||||
}
|
||||
|
||||
ssh_bind_free(bind);
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue