前言
兄弟萌, 我实现了一个实用的小工具, 特来分享.
事情刚开始是这样的, 我需要一个脚本来实现代码仓库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
内容.xxx
为header
名称- 将所有字母大写, 并将
-
替换为_
- 将所有字母大写, 并将
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
的方式来制作自己的镜像.
问题
启动子进程后台运行, 无法立即返回
问题
在脚本中使用如下命令启动子进程:
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-status
和add-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
内容.xxx
为header
名称
不同类型脚本读取系统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
的方式来制作自己的镜像.