From e014dca3cad0989a02ca11f9f569d0b1547d11e5 Mon Sep 17 00:00:00 2001 From: xxq250 Date: Mon, 15 Jun 2026 15:00:01 +0800 Subject: [PATCH] feat: add libssh git ssh gateway demo --- .gitignore | 11 + CMakeLists.txt | 25 ++ README.md | 327 ++++++++++++++++++++++ openresty/nginx.conf | 34 +++ scripts/gen-host-key.sh | 12 + src/git_ssh_gateway.c | 594 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1003 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 openresty/nginx.conf create mode 100755 scripts/gen-host-key.sh create mode 100644 src/git_ssh_gateway.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c363b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +build/ +build-*/ +cmake-build-*/ + +ssh_host_*_key +ssh_host_*_key.pub + +repositories/ +repo/ +*.log + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c96f279 --- /dev/null +++ b/CMakeLists.txt @@ -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 +) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3692b94 --- /dev/null +++ b/README.md @@ -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。 diff --git a/openresty/nginx.conf b/openresty/nginx.conf new file mode 100644 index 0000000..5c1fc97 --- /dev/null +++ b/openresty/nginx.conf @@ -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; + } +} + diff --git a/scripts/gen-host-key.sh b/scripts/gen-host-key.sh new file mode 100755 index 0000000..3315f26 --- /dev/null +++ b/scripts/gen-host-key.sh @@ -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" + diff --git a/src/git_ssh_gateway.c b/src/git_ssh_gateway.c new file mode 100644 index 0000000..7cf7ccc --- /dev/null +++ b/src/git_ssh_gateway.c @@ -0,0 +1,594 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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; +}