家里的路由器基本上折腾差不多了,用的是小厂家的x86工控机,硬盘也是捡的垃圾,想到自己时不时的升级/重启/异常断电,还是备份一下的配置文件比较稳妥。我用的路由系统的基于debian的VYOS rolling版本,折腾了半个月已经非常熟悉了,用起来比较爽也很稳定,基本满足自己的各种需求。系统自带了同步配置的功能,每次commit前会先同步配置,支持以下几种方式:

   http://<user>:<passwd>@<host>/<path>
   https://<user>:<passwd>@<host>/<path>
   ftp://<user>:<passwd>@<host>/<path>
   sftp://<user>:<passwd>@<host>/<path>
   scp://<user>:<passwd>@<host>/<path>
   tftp://<host>/<path>
   git+https://<user>:<passwd>@<host>/<path>

以上方式中,最简单的就是http方式了,经过抓取日志,vyos 会使用 "POST /config.boot-vyos.20240205_171540" 方式上传配置,其中uri是配置文件名,包体是明文的配置内容,正好我的服务器使用的是openresty,就用lua写了一个简单的文件服务器,接收vyos POST过来的配置。lua脚本实现了以下功能:

  1. 接收POST包体并保存到文件,文件名为POST请求的uri
  2. 使用http basic authentication的用户名密码认证

接收POST请求,直接接收POST包体然后使用io.open写入文件:

            local file
            local upload_path = '/var/www/html/vyos/'

                    ngx.req.read_body()
                        local file_name = ngx.var.uri
                    local data = ngx.req.get_body_data()
                        if file_name then
                    file = io.open(upload_path .. file_name, 'a+')
                    if not file then
                        ngx.log(ngx.ERR, "failed to open file ", file_name)
                        return
                    end
                end
            if data then
                    file:write(data)
                    file:close()
            else
                    ngx.log(ngx.ERR, "data is empty")
                    return
            end

用户名密码认证,使用nginx也能实现,但是认证文件要单独写进文件里。用lua就一个配置文件就能可以了。http basic auth主要是是解析http头Authorization,并使用base64解密用户名密码:

            local function split(str, delimiter)
                local result = {}
                if not str or not delimiter then
                    return result
                end

                for match in (str..delimiter):gmatch("(.-)"..delimiter) do
                    if match then
                        table.insert(result, match)
                    end
                end
                return result
            end

            local function parseHeader(authorization)
                local base64 = string.sub(authorization, 7)
                local data = split(ngx.decode_base64(base64), ":")
                return data[1], data[2]
            end

            local header = ngx.req.get_headers()["Authorization"]
            if header then
                local username, password = parseHeader(header)
                if username ~= "vyos.user" or password ~= "vyos.pass" then
                    ngx.header["WWW-Authenticate"] = [[Basic realm="restricted"]]
                    ngx.exit(401)
                end
            end

以上就是核心逻辑,经过测试ngx.req.get_body_data() 读请求包体,会偶尔出现读取不到直接返回 nil 的情况。还需要额外设置client_max_body_size 和client_body_buffer_size, 强制在内存中保存请求体。

完整的nginx配置文件如下:

server {
        listen 80;
    server_name
        backup.opswill.com
        ;

    access_log /var/log/backup_access.log main;
    error_log  /var/log/backup_error.log;

    client_max_body_size 100m;
    client_body_buffer_size 100m;

    location / {
        content_by_lua_block {

            local function split(str, delimiter)
                local result = {}
                if not str or not delimiter then
                    return result
                end

                for match in (str..delimiter):gmatch("(.-)"..delimiter) do
                    if match then
                        table.insert(result, match)
                    end
                end
                return result
            end

            local function parseHeader(authorization)
                local base64 = string.sub(authorization, 7)
                local data = split(ngx.decode_base64(base64), ":")
                return data[1], data[2]
            end

            local header = ngx.req.get_headers()["Authorization"]
            if header then
                local username, password = parseHeader(header)
                if username ~= "vyos.user" or password ~= "vyos.pass" then
                    ngx.header["WWW-Authenticate"] = [[Basic realm="restricted"]]
                    ngx.exit(401)
                end
            end

            local file
            local upload_path = '/var/www/html/vyos/'

                    ngx.req.read_body()
                        local file_name = ngx.var.uri
                    local data = ngx.req.get_body_data()
                        if file_name then
                    file = io.open(upload_path .. file_name, 'a+')
                    if not file then
                        ngx.log(ngx.ERR, "failed to open file ", file_name)
                        return
                    end
                end
            if data then
                    file:write(data)
                    file:close()
            else
                    ngx.log(ngx.ERR, "data is empty")
                    return
            end
        }

    }

}

用curl测试文件上传:

 curl -X POST -d"body-abd-opswill.com" "https://vyos.user:vyos.pass@backup.ipip.dev/test.txt"  -ivvvv

会在/var/www/html/vyos/下生成 text.txt文件,内容是body-abd-opswill.com。

至此,一个使用lua实现的简单的文件服务器就配置完成了,该文件服务器可以接收post上传文件到指定目录并实现了基本的用户名密码认证。