在使用 github actions 时,需要在构建成功后自动部署应用到家里的树莓派中,但是又不能 ssh 连接,所以想在树莓派中运行一个服务,提供一个接口给公网调用 (已经用 frp 暴露服务到公网),服务内部则调用 shell 来部署服务。
CGI
通用网关接口 (Common Gateway Interface, CGI) 是为提供网络服务而执行控制台应用(命令行)的程序,提供于服务器上实现动态网页的通用协议。通常情况下,一次请求对应一个 CGI 脚本的执行,生成一个 HTML。
简而言之,一个 HTTP 请求,从客户端经由 标准输入 发送到一个 CGI 程序。同时使用环境变量携带其它数据(例如:URL 路径,HTTP 头字段等)。
参考 Wiki 通用网关接口
CGI 程序通过标准输入接收请求数据,将结果写到标准输出返回给客户端。所以程序的标准输出需要满足 HTTP 协议格式:
Status: 200
Content-Type: text/plain
HELLO WORLD
使用下面 Javascript 函数设置响应数据:
/**
* Send response to client and exit process
* @param {string} body Http response body string
* @param {{status?: number, contentType?: string}} options
*/
async function respond(body, { status = 200, contentType = 'text/plain' } = {}) {
process.stdout.write(`\
Status: ${status}\r\n\
Content-Type: ${contentType}\r\n\
\r\n\
${body}\r\n\
`)
process.exit()
}
ZX
Bash 很不错,但是大家喜欢使用其它更方便的编程语言来编写脚本。Javascript 就是一个完美的选择,但是 Node.js 标准库使用起来很麻烦。zx 使用
child_process封装了易用的接口,自动转义参数。
环境准备
-
系统 Linux
-
安装 nginx
sudo apt install nginx -y -
安装 fcgiwrap
sudo apt install fcgiwrap -y -
安装 nodejs
根据需要选择不同版本,这里安装 17.x 版本。根据环境选择 ubuntu 或 debian 安装方式。其它安装方式见 nodejs。
# Using Ubuntu curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash - sudo apt-get install -y nodejs # Using Debian, as root curl -fsSL https://deb.nodesource.com/setup_17.x | bash - apt-get install -y nodejs -
全局安装 zx
npm i -g zx
Nginx 配置
location /cgi-bin/ {
# 重写路径去掉 `/cgi-bin/`
rewrite ^/cgi-bin/(.*)$ /$1.mjs break;
# 指定 cgi 脚本所在目录
root /etc/nginx/cgi-bin/src;
gzip off;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
fastcgi_read_timeout 300s;
include /etc/nginx/fastcgi_params;
}
上面的 rewrite 和 root 相互配合使用。例如请求地址 https://xxx.xxx.xxx/cgi-bin/test 会映射到脚本文件 /etc/nginx/cgi-bin/src/test.mjs 。
CGI 脚本
/etc/nginx/cgi-bin/src/test.mjs
#!/usr/bin/env zx
$.verbose = false
const SECRET = 'CFA7296E268E406794B0625DD7C994EB'
await (async function () {
// 获取 query 参数
const query = new URLSearchParams(process.env['QUERY_STRING'])
const secret = query.get('secret')
const action = query.get('action')
// 验证 secret
if (secret !== SECRET) {
await respond(`FORBIDDEN: secret ${secret}`, { status: 403 })
return
}
// 根据 action 执行操作
switch (action) {
case 'memory':
await respond(await $`free -m`)
break
default:
await respond(`Not Found: ${acton}`, { status: 404 })
}
})()
/**
* Send response to client and exit process
* @param {string} body Http response body string
* @param {{status?: number, contentType?: string}} options
*/
async function respond(body, { status = 200, contentType = 'text/plain' } = {}) {
process.stdout.write(`\
Status: ${status}\r\n\
Content-Type: ${contentType}\r\n\
\r\n\
${body}\r\n\
`)
process.exit()
}
注意需要给 CGI 脚本加上可执行权限
chmod +x test.mjs
调用 CGI 程序
curl https://xxx.xxx.xxx/cgi-bin/test?action=memory&secret=CFA7296E268E406794B0625DD7C994EB
调用结果
total used free shared buff/cache available
Mem: 7809 4208 190 4 3410 3139
Swap: 0 0 0