Nginx + fcgiwrap + nodejs + zx 搭建便捷服务端 API

989 阅读2分钟

在使用 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 版本。根据环境选择 ubuntudebian 安装方式。其它安装方式见 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;
}

上面的 rewriteroot 相互配合使用。例如请求地址 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