响应HTTP服务的shell脚本

前言

兄弟萌, 我实现了一个实用的小工具, 特来分享.

事情刚开始是这样的, 我需要一个脚本来实现代码仓库web hook的任务, 首先想到的是直接调用php, 但是php-fpm是以www-data用户运行的, 很多调用是无法实现的. 此时, 我就需要一个常驻的偶尔调用一下的, 能够以root用户执行的http服务器, 但是网上查了查, 发现并么有符合我要求的现成轮子, 于是它来了.

是什么

简单介绍一下这个轮子是做什么用的. 简单说, 就是启动一个执行shell脚本的http服务. 通过 shell 提供http服务.

它启动一个服务, 来监听指定端口并响应HTTP请求, 脚本在监听到新的请求后, 会根据请求路径以root身份调用相应的脚本执行任务并返回对应的内容.

如果需要HTTPS外边再套一层nginx就行了.

使用介绍

这个破玩意如何使用呢? 为了简化使用, 我将其打包成了docker镜像. 注意, 此服务每一次调用会启动一个新的进程, 故不适用于高并发场景. 当前介绍为最新版本使用指南, 历史版本内容在最下方(建议使用latest版本, 可直接忽略下方历史文档).

启动

假设脚本的本地运行目录为: /usr/share/script.

创建文件/usr/share/script/test.bash. 内容如下:

#!/usr/bin/env bash

echo "return"

给脚本赋予执行权限: chmod +x /usr/share/script/test.bash

docker命令行启动:

docker run -it -d -p 80:80 -v /usr/share/script:/opt/script hujingnb/http_cron
# 仅监听 127.0.0.1 的端口
docker run -it -p 127.0.0.1:80:80/tcp -v /usr/share/script:/opt/script hujingnb/http_cron

docker-composer启动:

version: '3.1'

services:
  tcp_cron:
    build: hujingnb/http_cron
    container_name: http_cron
    restart: always
    port: 
      - 80:80
    volumes:
      - /usr/share/script:/opt/script

OK, 此时访问请求: http:127.0.0.1/test, 就会看到返回内容return了.

脚本运行机制

路由分配

根据请求的request_uri调用对应的脚本.

若请求为: /user/change_name.

那么会将脚本的工作路径/opt/script(工作路径通过环境变量WORKSPACE修改) 与请求拼在一起, 拼接后的路径为: /opt/script/user/change_name, 依次寻找以下后缀文件, 首次找到的为执行脚本:

  • /opt/script/user/change_name.pl
  • /opt/script/user/change_name.sh
  • /opt/script/user/change_name.bash
  • /opt/script/user/change_name.php
  • /opt/script/user/change_name.py
  • /opt/script/user/change_name.rb

若没有找到脚本, 或访问根路径, 返回404.

注意, 所有脚本都需要赋予执行权限.

接收请求

脚本通过环境变量接收请求内容, bash脚本可直接通过$METHOD_TYPE读取. 有如下内容:

  • METHOD_TYPE: 请求的方法. GET POST
  • HTTP_VERSION: 请求的HTTP版本. HTTP/1.1
  • REQUEST_URI: 请求原始路径(去掉GET参数的).
  • QUERY_STR: GET请求的原始参数字符串
  • FORM_CONTENT: 若请求是POST, 则此变量保存请求体的字符串内容.
    • 没有对内容进行解析. 因为根据content-type不同, 解析方式不同. 请自行解析
    • 注意, 若内容过大, 可通过INIT_FORM_CONTENT将其关闭. 并从REQUEST_BODY_FILE获取请求内容
  • REQUEST_BODY_FILE: 用于临时存放请求内容的文件
    • INIT_FORM_CONTENT为1时(默认行为), 会将文件内容读出并放入环境变量FORM_CONTENT
  • QUERY_PARAM_xxx: 解析后的GET请求参数. xxx为参数名
  • HEADER_xxx: 请求的 header内容. xxxheader名称
    • 将所有字母大写, 并将-替换为_
  • REMOTE_ADDR: 客户端 IP 地址
  • REMOTE_PORT: 客户端端口

不同类型脚本读取系统env环境变量的方式不同, 请自行搜索.

响应请求

通过调用shell命令设置返回. 命令如下:

#!/usr/bin/env bash

# 设置响应码, 命令没有返回. 默认为200
# 多次调用取最后一次
set-status 200
# 添加返回的响应头信息
add-header <header_key> <header_value>

# 脚本所有的输出作为响应体返回
echo 1234

若是其他脚本请参考调用系统命令的方式,

配置

可通过如下配置进行自定义修改.

环境变量

  • WORKSPACE: 修改运行脚本的查找目录. 默认为: /opt/script.
  • INIT_FORM_CONTENT: 请求内容默认放在临时文件中, 脚本会将其读出并放入环境变量FORM_CONTENT中. 若请求太大, 需要自行读取, 可关闭.
    • 默认为1
    • 关闭时, 保存内容的文件路径在环境变量REQUEST_BODY_FILE
  • MAX_CONCURRENT: fcgiwrap启动进程数量, 可支持的最大并发数
    • 默认为4. 请根据机器性能酌情修改

若此配置无法满足, 请自行覆盖nginx配置文件. 路径: /etc/nginx/conf.d/default.conf

docker命令行通过-e参数添加环境变量

docker-composer通过environment参数修改环境变量

环境准备

为了保证镜像的大小, 只安装了必要的软件, 包括python PHP运行环境都没有. 故, 若你有额外需求的话, 有如下两种方式来实现系统环境的定制化:

1.覆盖/opt/init.bash文件

镜像在每次启动时, 都会首先执行/opt/init.bash文件, 可以在这里安装额外的软件等初始化操作.

请注意, /opt/init.bash脚本每次镜像启动都会执行一次. 此脚本默认不执行任何操作.

2. 镜像引用

你也可以通过FROM的方式来制作自己的镜像.

Dockerfile地址

Docker

问题

启动子进程后台运行, 无法立即返回

问题

在脚本中使用如下命令启动子进程:

sleep 60 &

查看到此命令立即返回, 但接口没有立即收到响应.

解决

需要将脚本的输入/输出/错误流都关闭. 解决方案如下:

// 方案一: 子进程启动时自己关闭
exec 0<&-
exec 1>&-
exec 2>&-

// 方案二: 启动子进程时关闭
sleep 60 1>&- 2>/dev/null &

或者你可以将结果重定向到其他文件. 对shell重定向不了解的可参考: https://hujingnb.com/archives/778

解决方案参考: https://github.com/gnosek/fcgiwrap/issues/40


历史版本

v1.2.4

2022-05-9 更新1.2.4版本. 当并发量过大时, 部分请求无法处理(当然也要根据机器性能来看), 之前最大支持并发4. 新增配置可修改.

改动

添加环境变量

  • MAX_CONCURRENT: fcgiwrap启动进程数量, 可支持的最大并发数
    • 默认为4. 请根据机器性能酌情修改

升级指南

可直接升级, 无需改动

v1.2.3

2022-05-06 更新1.2.3版本. 当请求内容过大是, 无法将其放入环境变量中, 只能放到文件中.

具体参阅: https://www.perlmonks.org/?node_id=684300

改动

当请求体超过64kb 时, 即使INIT_FORM_CONTENT为1, 也不会将其放入环境变量中, 并且会打印错误信息

升级指南

可直接升级, 无需改动

v1.2.2

2022-04-25 更新1.2.2版本. 为了解决传入内容过大时fastcgi报错.

报错内容:

fastcgi request record is too big

修改后将request_body使用临时文件缓存.

改动

新增环境变量

  • INIT_FORM_CONTENT: 请求内容默认放在临时文件中, 脚本会将其读出并放入环境变量FORM_CONTENT中. 若请求太大, 需要自行读取, 可关闭.
    • 默认为1
    • 关闭时, 保存内容的文件路径在环境变量REQUEST_BODY_FILE

去掉环境变量

  • CLIENT_BODY_BUFFER_SIZE: 不需要了, 现在不进行限制了
  • LISTEN_PORT: 可通过容器的80端口映射到外部其他端口来处理

升级指南

若手动修改过LISTEN_PORT, 需要自行将容器内部端口改为80

v1.2.1

2022-03-21 更新1.2.1版本. 为了解决当传入内容过大导致 nginx 报错的问题.

报错内容:

a client request body is buffered to temporary file

改动

nginx.conf添加配置:

  • client_max_body_size: 0 对客户端请求大小不进行限制
  • client_body_buffer_size: 默认8k.

添加环境变量

  • CLIENT_BODY_BUFFER_SIZE: 用来修改nginx.conf client_body_buffer_size 配置. 默认 8k

升级指南

无需任何修改

v1.2

2022-03-11 更新1.2版本. 为了将 socat替换为 nginx. 可提高运行的稳定性, 且支持优雅退出.

改动

实现机制修改, 原本是通过socat工具对端口进行监控, 修改为nginx+fcgiwrap的形式.

header参数修改

接收header, 通过环境变量HEADER_xxx进行读取. 区别是原本后面为header key本身内容, 现在将所有字母大写, 并将-替换为_. 例如原来的HEADER_test-header, 现在为HEADER_TEST_HEADER.

报错信息增加

原本当脚本没有执行权限的时候, 会正常返回响应码200. 现在修改为返回响应码500. 同时会将没有执行权限的报错信息输出到异常流中 (可通过 docker log 查看)

新增环境变量参数

  • REMOTE_ADDR: 客户端 IP 地址
  • REMOTE_PORT: 客户端端口

系统版本更新

debian buster 更新为 debian bullseye

升级指南

header的获取方式与之前不兼容. 因此需要将读取header的方式从HEADER_test-header修改为HEADER_TEST_HEADER.

其他内容均不需要修改

v1.1

2022-02-13 更新1.1版本. 为了解决response命令调用之前不能输出的问题.

改动

增加命令

  • set-status: 设置响应码.
    • 调用: set-status 200.
  • add-header: 添加响应头
    • 调用: add-header <header_key> <header-value>

response命令替换为了set-statusadd-header两个命令. 这两个命令没有任何输出, 可在进程运行的任何时刻调用. 脚本的所有输出均作为响应体返回.

升级指南

可不做任何修改直接进行升级, 对当前使用没有任何影响. 本次升级兼容1.0版本, 仍然可调用response命令, 只是不推荐.

建议

将命令

response --status=200 --header_header_key="header_value" "resp_content"

替换为

echo "resp_content"
set-status 200
add-header header_key header_value

v1.0

2021-12-26 发布1.0版本

1.0版本说明文档快照

启动

假设脚本的本地运行目录为: /usr/share/script.

创建文件/usr/share/script/test.bash. 内容如下:

#!/usr/bin/env bash

response "return"

给脚本赋予执行权限: chmod +x /usr/share/script/test.bash

docker命令行启动:

docker run -it -d -p 80:80 -v /usr/share/script:/opt/script hujingnb/http_cron

docker-composer启动:

version: '3.1'

services:
  tcp_cron:
    build: hujingnb/http_cron
    container_name: http_cron
    restart: always
    port: 
      - 80:80
    volumes:
      - /usr/share/script:/opt/script

OK, 此时访问请求: http:127.0.0.1/test, 就会看到返回内容return了.

脚本运行机制

路由分配

根据请求的request_uri调用对应的脚本.

若请求为: /user/change_name.

那么会将脚本的工作路径/opt/script(工作路径通过环境变量WORKSPACE修改) 与请求拼在一起, 拼接后的路径为: /opt/script/user/change_name, 依次寻找以下后缀文件, 首次找到的为执行脚本:

  • /opt/script/user/change_name.pl
  • /opt/script/user/change_name.sh
  • /opt/script/user/change_name.bash
  • /opt/script/user/change_name.php
  • /opt/script/user/change_name.py
  • /opt/script/user/change_name.rb

若没有找到脚本, 或访问根路径, 返回404.

注意, 所有脚本都需要赋予执行权限.

接收请求

脚本通过环境变量接收请求内容, bash脚本可直接通过$METHOD_TYPE读取. 有如下内容:

  • METHOD_TYPE: 请求的方法. GET POST
  • HTTP_VERSION: 请求的HTTP版本. HTTP/1.1
  • REQUEST_URI: 请求原始路径(去掉GET参数的).
  • QUERY_STR: GET请求的原始参数字符串
  • FORM_CONTENT: 若请求是POST, 则此变量保存请求体的字符串内容.
    • 没有对内容进行解析. 因为根据content-type不同, 解析方式不同. 请自行解析
    • 若内容过大, 可通过INIT_FORM_CONTENT将其关闭
  • REQUEST_BODY_FILE: POST请求时, 用于存放内容的文件路径
  • QUERY_PARAM_xxx: 解析后的GET请求参数. xxx为参数名
  • HEADER_xxx: 请求的 header内容. xxxheader名称

不同类型脚本读取系统env环境变量的方式不同, 请自行搜索.

响应请求

通过调用shell命令response进行返回. 如:

#!/usr/bin/env bash

# 注意, 脚本在所有输出之前, 必须先调用 response 命令

# status: 响应码. 默认为 200
# header_: 以 header_ 打头的参数为响应中添加的 header, 后面跟着 header 名. 可不传
# 最后的响应内容是必传参数. 若不需要, 可传空字符串
response --status=200 --header_ADD_HEADER=TEST "这里存放响应内容"
# 以极简的模式调用. 返回200, 并且没有响应体
# 若脚本全程没有审核输出, 则默认调用 response ""
# response ""

# 后续的所有 echo 都作为响应内容输出
echo $QUERY_STR

若是其他脚本请参考调用系统命令的方式, 需要将response的输出内容写到标准输出流.

配置

可通过如下配置进行自定义修改.

环境变量

  • WORKSPACE: 修改运行脚本的查找目录. 默认为: /opt/script.
  • LISTEN_PORT: 修改脚本监听的端口. 默认80

docker命令行通过-e参数添加环境变量

docker-composer通过environment参数修改环境变量

环境准备

为了保证镜像的大小, 只安装了必要的软件, 包括python PHP运行环境都没有. 故, 若你有额外需求的话, 有如下两种方式来实现系统环境的定制化:

1.覆盖/opt/init.bash文件

镜像在每次启动时, 都会首先执行/opt/init.bash文件, 可以在这里安装额外的软件等初始化操作.

请注意, /opt/init.bash脚本每次镜像启动都会执行一次. 此脚本默认不执行任何操作.

2. 镜像引用

你也可以通过FROM的方式来制作自己的镜像.

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x