#cgi #web-dev #multipart-form #response-body #env-var #http-response

dumb_cgi

适用于服务器端 CGI 程序的充足、无依赖的 CGI 库

4 个版本 (重大更改)

0.8.0 2023 年 7 月 19 日
0.7.0 2022 年 6 月 9 日
0.6.0 2022 年 5 月 18 日
0.5.0 2022 年 5 月 17 日

#471网络编程

MIT 许可证

56KB
636

dumb-cgi

一个充足、几乎无依赖的 Rust CGI 库。

该库的目的是允许服务器端 CGI 程序轻松解析请求(特别是“multipart/formdata”请求)并生成响应,而无需引入许多功能齐全的 crate。 dumb_cgi 不会尝试(或至少一开始不会尝试)成为资源高效的;其主要目标是简单易用。它做出的某些权衡包括

  • 它进行了大量的复制并创建了大量的小分配。这使得它更容易使用(和编写),但这也带来了性能和资源使用的代价。

  • 它强制将所有环境变量和标题名称和值进行有损转换到 UTF-8(以便可以将它们存储为 String)。规范应保证标题名称是有效的 UTF-8,但如果你需要任何其他三个(标题值、环境变量名称或环境变量值)是某种无法正确、有意义地转换为 UTF-8 的东西,那么这个 crate 对你的用例来说就太简单了。

  • 它不尝试解析格式不正确或几乎格式正确的请求,甚至可能为了简化实现而错误处理一些不常见的边缘情况。例如,multipart 主体部分中的标题和空白行必须以 "\r\n" 结尾(这是严格符合规范的),尽管其他许多 HTTP 实现仍然会识别普通的 "\n" 行结束符。这简化了实现。

  • 它的预期用例是仅限服务器端 CGI 程序。它支持 读取 请求,但不创建请求,支持 写入 响应,但不读取响应,并且仅支持与读取、解析和响应对 CGI 请求直接相关的 HTTP-verse 部分。

用法

为了说明读取和响应请求,以下是一个示例程序,它读取一个请求,记录其信息,然后返回一个简单的“成功”响应。如果任何 .unwrap().expect() 发生崩溃,则Web服务器将仅返回通用的500响应。

对于日志记录,我们将使用来自 log 日志接口和 simplelog 日志crate的功能(如果你使用 log 功能编译 dumb-cgi,这两个都将成为依赖)。

use dumb_cgi::{Request, EmptyResponse, Query, Body};
use simplelog::{WriteLogger, LevelFilter, Config};

fn main() {
    // Open the log file.
    WriteLogger::init(
        LevelFilter::max(),
        Config::default(),
        std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open("dumb_example.log")
            .unwrap()
    ).unwrap();
    
    // Gather info about the CGI request, including reading any body
    // (if present) from stdin.
    let request = Request::new().unwrap();
    
    // Log method request origin information.
    log::trace!(
        "Rec'd {} request from {} on port {}:",
        request.var("METHOD").unwrap_or("none"),
        request.var("REMOTE_ADDR").unwrap_or("nowhere"),
        request.var("SERVER_PORT").unwrap_or("-1")
    );
    
    // Log all the headers.
    //
    // The `Request::header()` method will return individual header values
    // (if present); the `Request::headers()` method will return an
    // iterator over all `(name, value)` header pairs.
    log::trace!("    Request headers:");
    for (name, value) in request.headers() {
        log::trace!("        {}: {}", name, value);
    }
    
    // If there's a query string, log info about it.
    //
    // The `Request::query()` method returns a reference to a
    // `dumb_cgi::Query` enum.
    match request.query() {
        Query::None => {
            log::trace!("    No query string.");
        },
        Query::Some(form) => {
            // If this variant is returned, then the query string was
            // parseable as `&`-separated `name=value` pairs, and the
            // contained `form` value is a `HashMap<String, String>`.
            log::trace!("    Form data from query string:");
            for (name, value) in form.iter() {
                log::trace!("        {}={}", name, value);
            }
        },
        Query::Err(e) => {
            // If this variant is returned, there was an error attempting
            // to parse the `QUERY_STRING` environment variable as a series
            // of `&`-separated `name=value` pairs.
            
            // `dumb_cgi::Error`s have a public `.details` member that is a
            // string with information about the error.
            log::trace!("    Error parsing query string: {}", &e.details);
            // You can still access the value of `QUERY_STRING` directly:
            log::trace!(
                "    Raw QUERY_STRING value: {}",
                request.var("QUERY_STRING").unwrap()
            );
        },
    }
    
    // If there's a body, log info about it.
    //
    // The `Request::body()` method returns a reference to a
    // `dumb_cgi::Body` enum.
    match request.body() {
        Body::None => {
            log::trace!("    No body.");
        },
        Body::Some(bytes) => {
            // Most valid bodies of properly-formed requests will return
            // this variant; `bytes` will be an `&[u8]`.
            log::trace!("    {} bytes of body.", bytes.len());
        },
        Body::Multipart(parts) => {
            // If the request has a properly-formed `Content-type` header
            // indicating `multipart/form-data`, and the body of the request
            // is also properly formed, this variant will be returned.
            //
            // The contained `parts` is a vector of `dumb_cgi::MultipartPart`
            // structs, one per part.
            log::trace!("    Multipart body with {} part(s).", parts.len());
        },
        Body::Err(e) => {
            // This variant will be returned if there is an error reading
            // the body.
            log::trace!("    Error reading body: {}", &e.details);
        },
    }
    
    // And we'll just put a blank line here in the log to separate
    // info about separate requests.
    log::trace!("");
    
    // Now that we've read and logged all the information we want from our
    // request, it's time to generate and send a response.
    //
    // Responses can be created with the builder pattern, starting with
    // an `EmptyResponse` (which has no body). In order to send a response
    // with a body, we need to call `EmptyResponse::with_content_type()`,
    // which turns our `EmptyResponse` into a `FullResponse`, which takes
    // a body.
    
    // Takes the HTTP response code.
    let response = EmptyResponse::new(200)
        // Headers can be added any time.
        .with_header("Cache-Control", "no-store")
        // Now we can add a body.
        .with_content_type("text/plain")
        // A body can be added this way; `FullResponse` also implements
        // `std::io::Write` for writing to the response body.
        .with_body("Success. Your request has been logged.")
        // Again, headers can be added any time.
        .with_header("Request-Status", "logged");
    
    // `FullResponse::respond()` consumes the response value and writes the
    // response to stdout.
    response.respond().unwrap();
}

显然,更多详细信息可以在文档中找到。

待办事项

这可能实际上是这个crate的最终形式(除了错误修复或可用性调整之外)。我有一些改进的想法,但它们都将这个项目危险地推向了“不那么笨”的领域。另一方面,我不太喜欢同时维护 dumb_cginot_quite_as_dumb_cgi 的想法,所以这很可能只是功能蔓延和长出瑕疵,直到我对它感到厌恶。

注释

  • v 0.3.0: 移除了对 lua-patterns 的依赖,因为尽管我喜欢这个想法,它也在其他项目中工作得很好,但它总是崩溃。现在 dumb_cgi 仅依赖于 log 日志接口(以及如果你实际上想进行一些日志记录并启用 log 功能的话,还有 simplelog)。

  • v 0.4.0: 添加了显式的查询字符串解析。

  • v 0.5.0: 添加了响应类型和功能。

  • v 0.6.0: 修改了 Error 类型和处理。

  • v 0.7.0: log 现在也是一个可选依赖。

  • v 0.8.0 实现了 std::error::Error 用于 dumb_cgi::Error;修复了 README.md 中的一个拼写错误。

依赖项

~0–7.5MB
~43K SLoC