源码路径:
https://sqlite.org/althttpd/dir?ci=tip (推荐)
https://github.com/jesrui/althttpd/tree/master (旧版本)
althttpd.md文件翻译
https://sqlite.org/althttpd/file?name=althttpd.md&ci=tip
althttpd简介:
轻量级 web 服务器,设计宗旨是追求简洁、安全和低资源占用。
设计理念 Design Philosophy
设计理念
Althttpd通常是通过xinetd、systemd或类似的工具启动的。针对每个传入的连接启动一个单独的进程,该进程完全专注于为该连接提供服务。单个althttpd进程将处理同一连接上的一个或多个HTTP请求。当连接关闭时,althttpd进程退出。
Althttpd还可以独立运行。Althttpd本身监听端口80以接收传入的HTTP请求(或端口443以接收传入的HTTPS请求),然后复制自身以处理每个传入的连接。每个连接仍然使用单独的进程处理。唯一的区别是连接处理程序进程现在是由主althttpd实例启动的,而不是由xinetd或systemd启动。
Althttpd没有配置文件。所有配置都使用一些命令行参数处理。这有助于保持配置的简单性,并减轻了通过错误配置的Web服务器引入安全漏洞的担忧。
由于每个althttpd进程只需为单个连接提供服务,althttpd是单线程的。此外,每个进程的生命周期仅为单个连接的持续时间,这意味着althttpd不需要过多担心内存泄漏。这些设计因素有助于保持althttpd源代码的简洁性,便于进行安全审计和分析。
用于服务TLS连接有两个选项:
- 可以使用定义了ENABLE_TLS宏并链接到-lssl -lcrypto的althttpd构建,并使用–cert fullchain.pem和–pkey privkey.pem标志启动。
- 可以通过外部连接服务(如stunnel4)启动althttpd,向althttpd传递-https 1标志,告诉它通过该服务“间接”以HTTPS模式运行。
首选第一种选项(使用内置TLS)。
TLS:传输层安全性协议(TLS),是一种网络协议,用于保护在两个通信应用程序之间传输的数据的安全性和隐私性。TLS的前身是安全套接层协议(SSL)。TLS和SSL之间存在一些技术差异,但它们的基本目标相同。
TLS的主要功能包括:数据加密、身份验证、数据完整性。
HTTPS(超文本传输安全协议)是在HTTP协议上使用TLS的变体,用于加密和保护Web页面的数据传输。
源代码
althttpd的完整源代码包含在一个单独的C代码文件中,没有依赖于标准C库之外的任何内容,只有在选择ENABLE_TLS选项时才需要OpenSSL。源代码文件命名为"althttpd.c"。要构建和安装althttpd,请运行以下命令:
gcc -Os -o /usr/bin/althttpd althttpd.c
-Os 用于指定编译器优化代码大小而非执行速度。
althttpd的源代码有详细的注释,并且易于访问。对于特殊需求,定制化应该相对容易。
要使用libssl构建带有内置TLS支持的althttpd:
gcc -Os -o /usr/bin/althttpd -fPIC -DENABLE_TLS althttpd.c -lssl -lcrypto
SQLite网站使用静态构建,因此在服务器上不需要安装OpenSSL。
创建使用Xinetd的设置
以下是sqlite.org上/etc/xinetd.d/http文件的完整内容,配置althttpd以在IPv4和IPv6上提供未加密的HTTP请求。您可以将其用作创建自己安装的模板。
如果您更愿意使用systemd,有一个附近的单独教程介绍如何执行该操作。但是,即使只是为了更好地了解althttpd的期望,您仍然可以查看此处的说明。
service http
{
port = 80
flags = IPv4
socket_type = stream
wait = no
user = root
server = /usr/bin/althttpd
server_args = -logfile /logs/http.log -root /home/www -user www-data
bind = 45.33.6.223
}
service http
{
port = 80
flags = REUSE IPv6
bind = 2600:3c00::f03c:91ff:fe96:b959
socket_type = stream
wait = no
user = root
server = /usr/bin/althttpd
server_args = -logfile /logs/http.log -root /home/www -user www-data
}
关键观察点在于端口80上的每个传入TCP/IP连接都会启动一个/usr/bin/althttpd的副本,并带有一些额外的参数,这些参数等于web服务器的配置。
请注意,althttpd作为超级用户运行。这不是必需的,但如果这样做,然后althttpd将自身移动到Web文档层次结构的根(例如,示例中的/home/www)的chroot监狱中,然后在读取任何内容之前放弃所有超级用户权限。-user选项告诉althttpd在进入chroot监狱后变为用户www-data。
-root选项(始终应为绝对路径)告诉althttpd在哪里找到文档层次结构。在sqlite.org的情况下,所有内容都是从/home/www提供的。在此文档层次结构的顶级是一堆以".website"结尾的目录。每个这样的目录都是一个单独的网站。目录的选择基于传入HTTP请求的Host参数。sqlite.org的目录部分列表如下:
3dcanvas_tcl_lang_org.website
3dcanvas_tcl_tk.website
androwish_org.website
canvas3d_tcl_lang_org.website
canvas3d_tcl_tk.website
cvstrac_org.website
default.website
fossil_scm_com.website
fossil_scm_hwaci_com.website
fossil_scm_org.website
system_data_sqlite_org.website
wapp_tcl_lang_org.website
wapp_tcl_tk.website
www2_alt_mail_net.website
www_androwish_org.website
www_cvstrac_org.website
www_fossil_scm_com.website
www_fossil_scm_org.website
www_sqlite_org.website
对于每个传入的HTTP请求,althttpd获取请求头中Host参数的文本,将其转换为小写,并将所有ASCII字母数字之外的字符更改为"_"。结果确定用于内容的子目录。如果没有匹配项,则将使用"default.website"目录作为回退。
例如,如果Host参数是"www.SQLite.org",则将其名称转换为"www_sqlite_org.website",并且这是用于提供内容的目录。如果Host参数是"fossil-scm.org",则使用"fossil_scm_org.website"目录。通常,两个或更多名称指向同一个网站。例如,fossil-scm.org、www.fossil-scm.org、fossil-scm.com和www.fossil-scm.com都是同一个网站。在这种情况下,通常只有一个目录是真正的目录,其他目录是符号链接。
在仅托管单个网站的最小安装中,只需有一个名为"default.website"的子目录即可。
在*.website目录内,要提供的文件由HTTP请求URI选择。标记为可执行的文件将作为CGI运行。以".scgi"结尾并且内容形式为"SCGI hostname port"的非可执行文件将SCGI请求中继到hostname:port。所有其他非可执行文件都按原样提供。
如果请求URI指定了*.website内部的目录名称,则althttpd会附加"/home"、“/index.html"和”/index.cgi",按顺序查找匹配项。
如果URI的前缀与可执行文件的名称相匹配,那么该文件将作为CGI运行。对于按原样传递的内容,MIME类型是通过使用编译到althttpd中的表从文件扩展名推断出来的。
支持使用Xinetd的HTTPS
从版本2.0(2022-01-16)开始,althttpd可以选择支持TLS加密连接。使用Xinetd设置HTTPS网站与HTTP网站非常相似。xinetd的适当配置是/etc/xinetd.d目录中名为"https"的单个文件,内容如下:
service https
{
port = 443
flags = IPv4
socket_type = stream
wait = no
user = root
server = /usr/bin/althttpd
server_args = -logfile /logs/http.log -root /home/www -user www-data -cert /etc/letsencrypt/live/sqlite.org/fullchain.pem -pkey /etc/letsencrypt/live/sqlite.org/privkey.pem
bind = 45.33.6.223
}
service https
{
port = 443
flags = REUSE IPv6
bind = 2600:3c00::f03c:91ff:fe96:b959
socket_type = stream
wait = no
user = root
server = /usr/bin/althttpd
server_args = -logfile /logs/http.log -root /home/www -user www-data -cert /etc/letsencrypt/live/sqlite.org/fullchain.pem -pkey /etc/letsencrypt/live/sqlite.org/privkey.pem
}
当然,您需要调整路径名和IP地址,使其适合您的特定安装。
这个https配置文件与之前的http配置文件相同,只有一些变化:
将服务名称从 “http” 更改为 “https”
将端口号从80更改为443
添加-cert和-pkey选项给althttpd,以便它知道在哪里找到适当的证书和私钥。
创建新的https配置文件后,只需重新启动xinetd(通常使用命令 “/etc/init.d/xinetd restart”),您的现有网站将立即具有HTTPS版本。
设置HTTPS使用Stunnel4
早期版本的althttpd不支持加密。在althttpd上加密网站的推荐方式是使用stunnel4。这些建议现在已经改变。我们现在建议您将althttpd更新到2.0版本或更高版本,并使用前一节中描述的xinetd技术。此部分保留供历史参考。
在sqlite.org网站上,/etc/stunnel/stunnel.conf文件的相关行是:
cert = /etc/letsencrypt/live/sqlite.org/fullchain.pem
key = /etc/letsencrypt/live/sqlite.org/privkey.pem
[https]
accept = :::443
TIMEOUTclose = 0
exec = /usr/bin/althttpd
execargs = /usr/bin/althttpd -logfile /logs/http.log -root /home/www -user www-data -https 1
这个设置与xinetd设置非常相似。一个关键的区别是使用"-https 1"选项告诉althttpd连接是加密的。这很重要,这样althttpd将知道为CGI程序设置HTTPS环境变量。
配置xinetd和stunnel4同时运行althttpd是可以的。事实上,这是SQLite.org网站的运行方式。对http://sqlite.org/的请求经过xinetd,对https://sqlite.org/的请求经过stunnel4。
独立运行
在作者的桌面工作站上,他的主目录下有一个子目录名为~/www/default.website。该子目录包含一组文件和CGI脚本。可以通过运行以下命令让althttpd在那里提供内容:
althttpd -root ~/www -port 8080
"-port 8080"选项告诉althttpd在独立模式下运行,监听端口8080。
althttpd的作者只在测试时使用独立模式。对于基于SSL的生产用途,建议使用内置的TLS支持或stunnel4。
带有HTTPS的独立运行
如果althttpd使用TLS支持构建,则可以使用以下选项告诉它以HTTPS模式运行:
althttpd -root ~/www --port 8043 --cert unsafe-builtin
此选项使用编译的自签名SSL证书,极不安全,仅用于测试目的。使用–cert选项指定自己的PEM格式SSL证书。–cert的参数可以是SSL私钥(通常命名为"privkey.pem")和证书链(通常命名为"fullchain.pem")的串联。或者,–cert可以指向只有fullchain.pem文件,而独立的–pkey选项可以指向privkey.pem文件。
使用自己的证书:
althttpd -root ~/www --port 8043 --cert fullchain.pem --pkey privkey.pem
请注意,证书在althttpd降低根权限之前就会被读取,因此证书可能存在于althttpd进程将在其下运行的非根用户无法访问的地方。
独立模式用于网站开发和测试
如果您在一个目录中有构成网站的各种HTML、JavaScript、CSS和其他资源文件,并且希望轻松测试这些文件,您可以输入以下命令:
althttpd --page index.html
在上述命令中,"index.html"是初始HTML页面的名称。此命令以独立模式启动althttpd,侦听它可以找到的第一个可用端口,并绑定到回环IP地址(127.0.0.1)。它还会自动在您的Web浏览器中打开一个新标签页,并指向"index.html"页面。
如果您在远程系统上开发网站,可以使用以下方式启动:
althttpd --popup
“–popup"选项与”–page"类似,但不限制IP地址为回环地址,并且不尝试启动新的Web浏览器标签页。
安全特性
为了防范恶意行为,althttpd对将提供的文件名称施加了一些限制。在请求URI中,除了字母数字和", -./:~“之外的所有字符都会转换为单个”_“。此外,如果请求URI的任何路径元素以”.“或”-“开头,althttpd都会始终返回404 Not Found错误。因此,只要文件名以”.“或”-"开头,就可以将辅助文件(例如由CGI使用的数据库或其他内容)放在文档层次结构中。
当althttpd返回404时,它会尝试确定请求是否恶意,如果它认为是,则可以选择临时阻止客户端的IP。
一个例外:尽管althttpd通常对任何以".“开头的路径元素的请求返回404 Not Found,但它允许以”/.well-known/“开头的URI请求。”/.well-known/“下面的文件或目录名称允许以”.“或”-“开头(但不允许以”…"开头)。此例外是为了允许LetsEncrypt验证对网站的拥有权。
基本身份验证
如果在内容层次结构的任何位置都存在名为"-auth"的文件,那么所有同级文件和所有低级目录中的文件都需要HTTP基本身份验证,其定义由"-auth"文件的内容确定。“-auth"文件是纯文本和面向行的。空行和以”#"开头的行将被忽略。其他行的含义如下:
-
http-redirect
如果存在http-redirect行,则会导致所有HTTP请求重定向为HTTPS请求。"-auth"文件按顺序读取和处理,因此在"http-redirect"行下面的行对于http请求永远不会被看到或处理。
-
https-only
如果存在https-only行,则表示仅允许HTTPS请求。任何HTTP请求都会导致404 Not Found错误。https-only行通常出现在http-redirect行之后。
-
realm NAME
此形式的单行建立基本身份验证的 “realm”。Web浏览器通常将realm名称显示为要求用户名和密码的对话框的标题。
-
user NAME LOGIN:PASSWORD
有多个用户行,每个有效用户对应一行。LOGIN:PASSWORD参数定义用户必须键入的用户名和密码以访问网站。密码是明文的 - HTTP基本身份验证不是最安全的身份验证机制。成功登录后,NAME被存储在REMOTE_USER环境变量中,以便可以被CGI脚本访问。NAME和LOGIN通常相同,但也可以不同。
-
anyone
如果遇到"anyone"行,表示允许任何请求,即使没有提供用户名和密码。这行与"http-redirect"组合使用,可以使所有普通HTTP请求在不要求登录凭据的情况下重定向到HTTPS。
基本身份验证示例
http://www.sqlite.org/ 网站在顶级目录下包含一个"-auth"文件,内容如下:
http-redirect
anyone
这个"-auth"文件导致所有HTTP请求都被重定向到HTTPS,而无需进一步的登录。(试试:访问 http://sqlite.org/ 并验证是否被重定向到 https://sqlite.org/。)
在 https://fossil-scm.org/private/ 存在一个"-auth"文件,内容如下:
realm Access To All Fossil Repositories
http-redirect
user drh drh:xxxxxxxxxxxxxxxx
当然,密码不是一行 “x” 字符。这演示了"-auth"文件的典型用法。对于单个用户,只要用户通过HTTPS而不是HTTP进入,就授予对"private"子目录中内容的访问权限。强烈建议对所有基本身份验证使用 “http-redirect” 行,因为密码包含在请求头中,如果通过HTTP发送请求,可能会被坏人截取和窃取。
日志文件
如果在althttpd命令行上给出了"-logfile"选项,那么对于每个HTTP请求,都会向指定的文件追加一行。日志文件采用RFC4180规定的逗号分隔值(CSV)格式。在源代码中有一条注释解释输出行中每个字段的含义。
日志文件是CSV格式的,因此可以使用类似下面的脚本将其轻松导入到SQLite中进行分析:
CREATE TABLE log(
date TEXT, /* 时间戳 */
ip TEXT, /* 源IP地址 */
url TEXT, /* 请求URI */
ref TEXT, /* 引用者 */
code INT, /* 结果代码,例如:200,404 */
nIn INT, /* 请求中的字节数 */
nOut INT, /* 回复中的字节数 */
t1 INT, t2 INT, /* 处理时间(用户、系统)毫秒 */
t3 INT, t4 INT, /* CGI脚本时间(用户、系统)毫秒 */
t5 INT, /* 墙钟时间,毫秒 */
nreq INT, /* 此请求的序列号 */
agent TEXT, /* 用户代理 */
user TEXT, /* 远程用户 */
n INT, /* 在SCRIPT_NAME中的URL字节数 */
lineno INT /* 生成日志条目的源代码行 */
);
.mode csv
.import httplog.csv log
在"-logfile"选项上的文件名可能包含strftime()扩展的基于时间的字符。因此,要使每天使用一个新的日志文件,可以使用类似以下的内容:
-logfile /var/logs/althttpd/httplog-%Y%m%d.csv
客户端IP阻止
如果在althttpd中包含了–ipshun DIRECTORY选项,DIRECTORY是从chroot jail内部访问的绝对路径(以"/"开头),并且客户端的IP地址作为该目录中的一个文件出现,那么althttpd可能返回503 Service Unavailable而不是处理请求。
如果文件大小为零字节,则始终返回503。因此,您可以"touch"一个文件,其名称是IP地址,永久封禁该客户端。
如果文件大小为N字节,则如果文件的修改时间在300*N秒之前,则在N秒内返回503。换句话说,每字节文件封禁客户端五分钟。
如果althttpd收到一个将导致404 Not Found的请求,并且在检查REQUEST_URI时该请求看起来很可疑,那么会自动创建封禁文件。例如,任何包含/…/的请求都被视为黑客企图。还检查其他常见的漏洞探测。可能会根据经验增加漏洞探测列表。
封禁文件在5分钟/字节后自动取消链接。
封禁文件最初大小为1字节。但如果封禁到期,然后在5分钟每字节的块文件大小之前到达新请求,则文件将增加一个字节,并且修改时间将被重置。
5分钟的封禁时间可以在构建时通过传递 -DBANISH_TIME=N 进行配置,其中 N 是默认为 300 的秒数。
GZip内容压缩
Althttpd对服务器端内容压缩有基本支持,通常将文件的传输成本减少一半以上。与将压缩库添加到althttpd不同,它依赖于网站开发人员提供压缩和未压缩形式的内容。
在提供文件时,如果客户端支持gzip压缩,并且找到了同名文件加上 .gz 扩展名,就会向客户端提供已gzip压缩的文件,并附带响应头表示已经gzip压缩。对于用户来说,原始请求的文件似乎是压缩的。但实际上,在幕后,提供的是不同的文件。
请注意,此功能仅适用于静态文件,而不适用于CGI。
static-build.md文件翻译
https://sqlite.org/althttpd/file?name=static-build.md&ci=tip
构建独立二进制文件
本文档描述了如何在Linux(或类似的类Unix操作系统)上构建一个完全自包含的、静态链接的 “althttpd” 二进制文件。
所需材料
- 从本网站获取althttpd.c源代码文件。
- 最新版的OpenSSL发布版的tar包。
- 常见的Unix构建工具,如 “make”、“gcc” 等。
- OpenSSL构建需要Perl。
构建步骤
-
编译静态的OpenSSL库。
- 解压OpenSSL的tar包。
- 将顶层目录重命名为 “openssl”。
- 进入 openssl 目录。
- 运行以下命令:
./config no-ssl3 no-weak-ssl-ciphers no-shared no-threads --openssldir=/usr/lib/ssl CFLAGS=-Os make -e
- 在OpenSSL构建过程中,可以边喝茶边等待。
-
编译althttpd。
- 返回到存放althttpd.c源文件的顶层目录。
- 运行:
gcc -I./openssl/include -Os -Wall -Wextra -DENABLE_TLS \ -o althttpd althttpd.c -L./openssl -lssl -lcrypto -ldl
- 上述命令构建了一个二进制文件,该文件静态链接到OpenSSL,但仍然动态链接到系统库。要使二进制文件完全静态,请添加 -static 选项。
- 在上述gcc命令中,您还可以选择添加 -DALTHTTPD_VERSION=‘…’,引号中的文本是将报告给CGI脚本的althttpd版本号。
使用二进制文件
以上就是所有需要做的。编译后,只需将生成的二进制文件移动到服务器的 /usr/bin 目录即可。
linode-systemd.md文件翻译
https://sqlite.org/althttpd/file?name=linode-systemd.md&ci=tip
使用Althttpd、Linode和Systemd设置网站
以下是我最近(2024年1月16日)在廉价的Linode VPS上使用Alhttpd、Let’s Encrypt和systemd设置网站的笔记。
如果对本文档有建议或错误报告,请在论坛上发表留言。
1.0 创建Linode帐户并启动VPS
在Linode上创建帐户并启动新的VPS。您可以以每月5美元的价格获得它们。在这个示例中,我使用了一个每月12美元的Linode,这可能有点过剩。我可能会在某个时候降级到每月5美元的计划。
我选择了Ubuntu 23.10作为操作系统,因为那是当时最新的Ubuntu版本。使用您更熟悉的其他Linux发行版。
2.0 注册您的域名
我构建的系统使用了我已经拥有的域名的子域,因此我不必购买新的域名。如果不是这种情况,请立即购买域名。如何执行此操作的详细信息超出了本文档的范围,但网络上有许多好的教程。
将您的域名指向Linode名称服务器。根据您要执行的操作,进行所需的任何DNS条目。Linode提供了一个出色且易于使用的界面。
3.0 初始系统配置
以root身份登录到您的VPS。我需要升级和安装新软件,如下所示:
apt update
apt upgrade
apt install letsencrypt
4.0 创建Web服务器用户和主目录
应该已经有一个名为 “www-data” 的用户。(检查 /etc/passwd 文件。)这是我用于Web服务的用户。
我还手动编辑了 /etc/passwd 文件,将用户 www-data 的默认shell从 /usr/bin/nologin 更改为 /bin/bash。这个更改允许我运行 “su www-data” 命令以成为 www-data 用户,同时操作属于该用户的文件。但这一步是完全可选的。
为该用户在 /home/www 创建一个主目录。创建一个名为 /home/www/default.website 的子目录。将子目录的所有者更改为 www-data。命令序列如下:
mkdir -p /home/www/default.website
chown www-data /home/www/default.website
创建一个名为 /home/www/default.website/index.html 的文件,该文件由 www-data 用户可读,并放入一些HTML内容作为占位符。也许像这样:
<h1>Hello, World</h1>
<p>If you can see this, that means the web server is running.</p>
添加任何您想要的其他内容。但是,请注意,具有执行权限位的文件将被视为CGI运行。因此,请确保 *.website 文件夹中的任何文件或子文件夹中的文件都不可执行,除非您确实打算将它们作为CGI运行。
5.0 为althttpd构建二进制文件
我在我的桌面Linux机器上构建了一个静态链接的althttpd二进制文件(使用这些说明),并使用scp将静态二进制文件传输到VPS。在 /usr/bin/ 安装静态二进制文件。
6.0 用于HTTP服务的Systemd设置
首先,您需要启动简单的HTTP(未加密)服务,因为这是从Let’s Encrypt获取证书的先决条件。创建一个文件 /etc/systemd/system/http.socket,内容如下:
[Unit]
Description=HTTP socket
[Socket]
Accept=yes
ListenStream=80
NoDelay=true
[Install]
WantedBy=sockets.target
然后创建另一个文件,命名为 /etc/systemd/system/http@.service,内容如下:
[Unit]
Description=HTTP socket server
After=network-online.target
[Service]
WorkingDirectory=/home/www
ExecStart=/usr/bin/althttpd -root /home/www -
user www-data
StandardInput=socket
[Install]
WantedBy=multi-user.target
文件名中的 “@” 不是拼写错误。由于某种原因,systemd似乎需要它。我不知道细节。
最后,使用以下命令启动新服务:
systemctl daemon-reload
systemctl enable http.socket
systemctl start http.socket
此时,您应该能够将Web浏览器指向VPS的端口80,并看到在步骤4.0末尾安装的占位HTML。您还可以使用以下命令检查服务的状态或关闭服务:
systemctl status http.socket
systemctl stop http.socket
7.0 获取Let’s Encrypt证书
为了使用HTTPS,您需要一个证书。使用类似以下的命令获取证书:
letsencrypt certonly --webroot -w /home/www/default.website -d your-domain.org
当然,将您的实际域名替换为 “your-domain.org”。如果您希望Web服务器为多个域提供服务,可以使用多个 “-d” 选项。有关详细信息,请参阅letsencrypt文档。
我相信此命令设置了域将自动更新证书。您不应该再次运行此命令。如果以后发现我错了,我将回来更正这段文字。
您的证书将在以下文件中找到:
/etc/letsencrypt/live/your-domain.org/fullchain.pem
/etc/letsencrypt/live/your-domain.org/privkey.pem
确保 “privkey.pem” 文件保持安全。这是您的私钥。这是Web服务器用于向陌生人验证您服务器身份的内容。
8.0 激活HTTPS
现在您有了证书,您可以创建额外的systemd配置条目,以在端口443上接收TLS HTTPS请求。首先创建一个名为 /etc/systemd/system/https.socket 的文件,内容如下:
[Unit]
Description=HTTPS socket
[Socket]
Accept=yes
ListenStream=443
NoDelay=true
[Install]
WantedBy=sockets.target
然后创建另一个文件,命名为 /etc/systemd/system/https@.service,内容如下:
[Unit]
Description=HTTPS socket server
After=network-online.target
[Service]
WorkingDirectory=/home/www
ExecStart=/usr/bin/althttpd -root /home/www -user www-data -cert /etc/letsencrypt/live/your-domain.org/fullchain.pem -pkey /etc/letsencrypt/live/your-domain.org/privkey.pem
StandardInput=socket
[Install]
WantedBy=multi-user.target
这两个文件与步骤6.0中为HTTP服务创建的文件非常相似。关键区别:
- 文件名中说 “https” 而不是 “http”。
- TCP端口是443而不是80。
- althttpd 命令有额外的 -cert 和 -pkey 参数来标识您的证书。
创建这两个文件后,运行:
systemctl daemon-reload
systemctl enable https.socket
systemctl start https.socket
执行完这些步骤后,您应该能够使用TLS加密的连接浏览您的网站。
9.0 记录Web流量
如果要记录Web流量(建议),请创建一个名为 /home/www/log 的新目录,并将所有者更改为 www-data。然后编辑步骤6.0和8.0中创建的 *.service 文件,将 “–log /log/http.log” 一词添加到 “ExecStart=…” 行中。
请注意,文件名为 “/log/http.log”,而不是 “/home/www/log/http.log”。这是因为althttpd在执行任何其他操作之前会对 /home/www 目录进行chroot。这是一个安全功能,可防止CGI脚本中的错误危害您的系统。由于chroot,从althttpd的角度来看, “/home/www/log/http.log” 文件实际上将被称为 “/log/http.log”。
在进行这些更改后,运行:
systemctl restart http.socket
systemctl restart https.socket
10.0 其他配置更改
现在,您可以尝试其他配置更改。添加像 “–ipshun /ipshun” 这样的选项是推荐的,以帮助消除恶意爬虫。您还可以在 “/home/www” 下添加特定域的新 “*.website” 文件夹。有关指导和建议,请参阅其他althttpd文档。本教程应足以帮助您入门。
Makefile文件解释
default: althttpd althttpsd # 构建目标
SHELL = /bin/bash # 指定shell
VERSION_NUMBER = 2.0
CC=cc # c编译器
CFLAGS=-Os -Wall -Wextra
# 检查是否安装了 fossil(版本控制工具),如果安装了,则使用 fossil 更新版本。
manifest: # 只是一个标签
@if which fossil > /dev/null; then \
fossil update --nosync current; \
else \
echo "fossil binary not found. Version hash/time might be incorrect."
fi
# 定义一个目标 manifest.uuid,它依赖于 manifest。这个目标的规则是空的
manifest.uuid: manifest
# We do the version-setting CPPFLAGS this way, instead of via
# $(shell...), for the sake of portability with BSD Make. Some hoops
# have to be jumped through to get the escaping "just right," though.
# 读取 manifest.uuid 文件中的版本信息和哈希,并将其写入名为 version 的文件中。
version: Makefile manifest.uuid
@hash=`cut -c1-12 manifest.uuid`; \
time=`sed -n 2p manifest | cut -d' ' -f2`; \
{ echo -n "ALTHTTPD_VERSION=\""; \
echo '$(VERSION_NUMBER)' "[$$hash] [$$time]\""; \
} > $@
# 使用从 version 文件中读取的标志进行编译
althttpd: althttpd.c version
@flags="`cat version`"; set -x; \
$(CC) $(CFLAGS) "-D$$flags" -o althttpd althttpd.c
# 相对于althttpd,加了TLS、SSL、crypto、-fPIC
althttpsd: althttpd.c version
@flags="`cat version`"; set -x; \
$(CC) $(CFLAGS) "-D$$flags" -fPIC -o althttpsd -DENABLE_TLS althttpd.c -lssl -lcrypto
# 静态编译,避免运行时依赖问题。
static-althttpd: althttpd.c version
@flags="`cat version`"; set -x; \
$(CC) $(CFLAGS) "-D$$flags" -static -o althttpd althttpd.c
# 静态编译,避免运行时依赖问题。
static-althttpsd: althttpd.c version
@flags="`cat version`"; set -x; \
$(CC) $(CFLAGS) "-D$$flags" -static -fPIC -o althttpsd -DENABLE_TLS althttpd.c -lssl -lcrypto -lpthread -ldl
clean:
rm -f althttpd althttpsd version
extract-error-numbers.tcl 文件解释
tcl脚本,用于从althttpd.c源文件中提取每个日志条目末尾的错误号。
比较类似python、bash脚本
#!/usr/bin/tclsh # 指定脚本的解释器,Tcl解释器。
# Run this script to extract the error numbers that appear at the end of each log file entry from the source text.
# 运行这个脚本可以从源文本中提取日志条目末尾的错误号。
set in [open althttpd.c r] # 打开名为 althttpd.c 的文件用于读取(r表示只读),文件句柄in。
while {![eof $in]} { # 直到文件结束
set line [gets $in] # 从文件中读取一行,并将其存储在变量 line 中。
# 使用正则表达式匹配来检查是否在当前行中存在特定的日志格式。
# 如果匹配成功,将匹配到的错误号存储在变量 num 中,将日志消息存储在变量 msg 中。
if {[regexp {(\d+)[^0-9]+/\* LOG: (.*) \*/} $line all num msg]} {
# 如果匹配成功,打印一个SQL语句,将错误号和消息插入到名为 xref 的表中。
puts "INSERT INTO xref VALUES($num,'$msg');"
}
}
close $in # 关闭文件句柄,释放文件资源。
static-ssl.mk 文件解释
用于构建静态链接 OpenSSL 版本的 althttpd 的 Makefile 文件。
# 2022-02-16:
# This makefile is used by the author (drh) to build versions of
# althttpd that are statically linked against OpenSSL. The resulting
# binaries power sqlite.org, fossil-scm.org, and other machines.
# 说明该 Makefile 由作者(drh)用于构建静态链接 OpenSSL 版本的 althttpd,用于驱动 sqlite.org、fossil-scm.org 等网站。
# This is not a general-purpose makefile. But perhaps you can adapt it to your specific needs.
#
default: althttpd # 默认目标是构建 althttpd 可执行文件。
VERSION_NUMBER ?= 2.0 # 定义变量
# 定义变量 VERSION_HASH,使用 shell 命令从 manifest.uuid 文件中提取前12个字符。
VERSION_HASH ?= $(shell cut -c1-12 manifest.uuid)
# 定义变量 VERSION_TIME,使用 shell 命令从 manifest 文件的第二行提取时间。
VERSION_TIME ?= $(shell sed -n 2p manifest | cut -d' ' -f2)
# 定义变量 ALTHTTPD_VERSION,包含版本号、哈希和时间。
ALTHTTPD_VERSION ?= "$(VERSION_NUMBER) [$(VERSION_HASH)] $(VERSION_TIME)"
# 在预处理阶段添加一个宏定义,将 ALTHTTPD_VERSION 的值传递给 C 源文件。
CPPFLAGS += -DALTHTTPD_VERSION='$(ALTHTTPD_VERSION)'
# 如果安装了fossil ,则使用 fossil 更新版本。
manifest:
@if which fossil > /dev/null; then \
fossil update --nosync current; \
else \
echo "fossil binary not found. Version hash/time might be incorrect."
fi
manifest.uuid: manifest
# 指定 OpenSSL 的安装路径
OPENSSLDIR = /home/drh/fossil/release-build/compat/openssl
# 指定链接 OpenSSL 库时需要的库选项。
OPENSSLLIB = -L$(OPENSSLDIR) -lssl -lcrypto -ldl
# 启用 TLS 并指定 OpenSSL 头文件路径
CPPFLAGS += -I$(OPENSSLDIR)/include -DENABLE_TLS
CPPFLAGS += -Wall -Wextra
CFLAGS = -Os
althttpd: althttpd.c manifest
gcc $(CPPFLAGS) $(CFLAGS) -o althttpd althttpd.c $(OPENSSLLIB)
clean:
rm -f althttpd
源码中将ALTHTTPD_VERSION当作字符串使用
#define SERVER_SOFTWARE "althttpd " ALTHTTPD_VERSION
#define SERVER_SOFTWARE_TLS SERVER_SOFTWARE ", " OPENSSL_VERSION_TEXT
static char *zServerSoftware = 0;/* Software name and version info */
puts(SERVER_SOFTWARE_TLS);
zServerSoftware = useHttps==2 ? SERVER_SOFTWARE_TLS : SERVER_SOFTWARE;
althttpd.c 源码分析
althttpd.c 源码注释部分翻译
注释描述了一个小型、简单、独立的 HTTP 服务器的源代码文件。
/*
** 2001-09-15
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
**
** This source code file implements a small, simple, stand-alone HTTP server.
** 小型、简单、独立的 HTTP 服务器
** Features: 特点
**
** * Launched from inetd/xinetd/systemd, or as a stand-alone server
** * One process per TCP/IP connection
** * Deliver static content or run CGI or SCGI
** * Virtual sites based on the "Host:" property of the HTTP header
** * Runs in a chroot jail
** * Unified log file in a CSV format
** * Small code base (this 1 file) to facilitate security auditing
** * Simple setup - no configuration files to misconfigure
可以从 inetd/xinetd/systemd 启动,也可以作为独立服务器。
每个 TCP/IP 连接一个进程。
提供静态内容或运行 CGI 或 SCGI。
基于 HTTP 头的 "Host:" 属性的虚拟站点。
在 chroot jail 中运行。
统一的 CSV 格式日志文件。
小型代码库,有助于进行安全审计。
简单设置 - 无需配置文件。
**
** This file implements a small and simple but secure and effective web
** server. There are no frills. Anything that could be reasonably
** omitted has been.
该文件实现了一个小巧简单、安全有效的 Web 服务器。没有多余的装饰,一切可以合理省略的都已省略。
**
** Setup rules: 设置规则
**
** (1) Launch as root from inetd/systemd like this:
** 作为 root 从 inetd/systemd 启动,使用如下命令:
** althttpd -logfile logfile -root /home/www -user nobody
**
** It will automatically chroot to /home/www and become user "nobody".
** The logfile name should be relative to the chroot jail.
** 它将自动进入 chroot jail,切换到 /home/www 目录,并成为用户 "nobody"。
日志文件名应相对于 chroot jail。
** (2) Directories of the form "*.website" (ex: www_sqlite_org.website)
** contain content. The directory is chosen based on the HTTP_HOST
** request header. If there is no HTTP_HOST header or if the
** corresponding host directory does not exist, then the
** "default.website" is used.
形如 "*.website" 的目录(例如:www_sqlite_org.website)包含内容。
目录的选择基于 HTTP_HOST 请求头。如果没有 HTTP_HOST 请求头,
或者对应的主机目录不存在,则使用 "default.website"。
**
** In stand-alone mode (when the --port, --page, or --popup options are
** used) if neither the HTTP_HOST.website nor "default.website"
** directories exist, then files are served directly from the
** directory from which althttpd was launched. This makes it easy
** to run a quick web-server for testing purposes using commands like:
** 在独立模式下(当使用 --port、--page 或 --popup 选项时),
如果既不存在 HTTP_HOST.website 目录,也不存在 "default.website" 目录,
那么文件将直接从启动 althttpd 的目录提供服务。
这样可以轻松地通过诸如以下命令运行快速的用于测试目的的 Web 服务器:
** althttpd --page fiddle.html
** althttpd --popup
** althttpd --port 8080..8090
**
** In one-connection mode (when launched from xinetd or similar)
** an error is raised if "default.website" does not exist.
在单连接模式下(从 xinetd 或类似的工具启动时),如果 "default.website" 不存在,将引发错误。
**
** If the HTTP_HOST header contains any charaters other than
** [a-zA-Z0-9_.,*~/] then a 403 error is generated.
如果 HTTP_HOST 请求头包含除 [a-zA-Z0-9_.,*~/] 之外的任何字符,将生成 403 错误。
**
** (3) Any file or directory whose name begins with "." or "-" is ignored,
** except if the URL begins with "/.well-known/" then initial "." and
** "-" characters are allowed, but not initial "..". The exception is
** for RFC-5785 to allow letsencrypt or certbot to generate a TLS cert
** using webroot.
任何文件或目录名以 "." 或 "-" 开头的都会被忽略,但如果 URL 以 "/.well-known/" 开头,
则允许使用初始的 "." 和 "-" 字符,但不允许使用初始的 ".."。
这是为了 RFC-5785 的例外,以允许 letsencrypt 或 certbot 使用 webroot 生成 TLS 证书。
**
** (4) Characters other than [0-9a-zA-Z,-./:_~] and any %HH characters
** escapes in the filename are all translated into "_". This is
** a defense against cross-site scripting attacks and other mischief.
文件名中除了 [0-9a-zA-Z,-./:_~] 和任何 %HH 字符转义之外的字符都会被翻译成 "_”。
这是为了防范跨站脚本攻击和其他恶意行为。
**
** (5) Executable files are run as CGI. Files whose name ends with ".scgi"
** trigger an SCGI request (see item 9 below). All other files
** are delivered as is.
可执行文件会被作为 CGI 运行。
文件名以 ".scgi" 结尾的会触发一个 SCGI 请求(参见下面的第9项)。
所有其他文件将按原样传送。
**
** (6) If a file named "-auth" exists in the same directory as the file to
** be run as CGI/SCGI or to be delivered, then it contains information
** for HTTP Basic authorization. See file format details below.
如果与将要作为 CGI/SCGI 运行或传送的文件位于同一目录中存在一个名为 "-auth" 的文件,
那么它包含了 HTTP 基本授权的信息。请参阅下面的文件格式详细信息。
**
** (7) To run as a stand-alone server, simply add the "-port N" command-line
** option to define which TCP port to listen on. If the argument is
** "--port N1..N2" then TCP ports between N1 and N2 are scanned looking
** for one that is open and the first open port is used.
要作为独立服务器运行,只需添加 "-port N" 命令行选项来定义要监听的 TCP 端口。
如果参数是 "--port N1..N2",则会扫描 N1 和 N2 之间的 TCP 端口,找到第一个开放的端口并使用。
**
** (8) For static content, the mimetype is determined by the file suffix
** using a table built into the source code below. If you have
** unusual content files, you might need to extend this table.
对于静态内容,文件的 MIME 类型由下面源代码中内置的表格来确定,它使用文件后缀。
如果你有不寻常的内容文件,可能需要扩展这个表格。
**
** (9) Content files that end with ".scgi" and that contain text of the
** form "SCGI hostname port" will format an SCGI request and send it
** to hostname:port, then relay back the reply. Error behavior is
** determined by subsequent lines of the .scgi file. See SCGI below
** for details.
以 ".scgi" 结尾的内容文件,如果包含形如 "SCGI hostname port" 的文本,
将格式化一个 SCGI 请求并将其发送到 hostname:port,然后中继回响应。
错误行为由 .scgi 文件的后续行决定。有关详细信息,请参阅下面的 SCGI 部分。
**
** (10) If compiled with -DENABLE_TLS and linked against OpenSSL and
** launched with a --cert option to identify a certificate file, then
** TLS is used to encrypt the connection.
如果使用 -DENABLE_TLS 进行编译,并链接到 OpenSSL,
并使用 --cert 选项启动以确定证书文件,则会使用 TLS 加密连接。
**
** (11) When requesting static file NAME and if the HTTP request includes
** "Accept-Encoding: gzip" and if file "NAME.gz" exists on disk, then
** return NAME.gz with an "Content-encoding: gzip" header
在请求静态文件 NAME 时,如果 HTTP 请求包含 "Accept-Encoding: gzip",
并且磁盘上存在文件 "NAME.gz",那么返回 NAME.gz 并带有 "Content-Encoding: gzip" 头。
**
** Command-line Options:命令行选项
**
** --root DIR Defines the directory that contains the various
** $HOST.website subdirectories, each containing web content
** for a single virtual host. If launched as root and if
** "--user USER" also appears on the command-line and if
** "--jail 0" is omitted, then the process runs in a chroot
** jail rooted at this directory and under the userid USER.
** This option is required for xinetd launch but defaults
** to "." for a stand-alone web server. DIR should always
** be an absolute path, else child processes might misbehave.
定义包含各种 $HOST.website 子目录的目录,每个子目录包含单个虚拟主机的 Web 内容。
**
** --port N Run in standalone mode listening on TCP port N, or from
** --port N1..N2 the first available TCP port in the range from N1 to N2.
以独立模式运行,监听 TCP 端口 N 或 N1 到 N2 范围内的第一个可用端口。
**
** --user USER Define the user under which the process should run if
** originally launched as root. This process will refuse to
** run as root (for security). If this option is omitted and
** the process is launched as root, it will abort without
** processing any HTTP requests.
定义进程应以哪个用户身份运行。
**
** --logfile FILE Append a single-line, CSV-format, log file entry to FILE
** for each HTTP request. FILE should be a full pathname.
** The FILE name is interpreted inside the chroot jail. The
** FILE name is expanded using strftime() if it contains
** at least one '%' and is not too long.
为每个 HTTP 请求在 FILE 中追加一行 CSV 格式的日志条目。
**
** --ipshun DIR If the remote IP address is also the name of a file
** in DIR that has size N bytes and where either N is zero
** or the m-time of the file is less than N time-units ago
** then that IP address is being shunned and no requests
** are processed. The time-unit is a compile-time option
** (BANISH_TIME) that defaults to 300 seconds. If this
** happens, the client gets a 503 Service Unavailable
** reply. Furthermore, althttpd will create ip-shunning
** files following a 404 Not Found error if the request
** URI is an obvious hack attempt. The ip-shunning file
** will also be created if a CGI returns status code 418.
如果远程 IP 地址在 DIR 中的文件名中,该文件大小为 N 字节,
且 N 为零或文件的修改时间少于 N 时间单位,则该 IP 地址被屏蔽。
**
** --https BOOLEAN Indicates that input is coming over SSL and is being
** decoded upstream, perhaps by stunnel. This option
** does *not* activate built-in TLS support. Use --cert
** for that.
指示输入是否通过 SSL,但不激活内置 TLS 支持。
**
** --page NAME Come up in stand-alone mode, and then try to launch a
** web-browser pointing to the NAME document after the
** listening socket has been created. This option
** implies --loopback and "--port 8080..8100".
在独立模式下启动,并尝试在创建监听套接字后启动一个指向 NAME 文档的 Web 浏览器。
**
** --popup Launch a stand-alone web server to use for testing.
** This option implies "--port 8080..8100". This option
** is similar to "--page NAME" except that it does not
** try to launch a web-browser and does not force the
** connection into --loopback mode. Use this when
** running a test web-server on a remote host via ssh.
启动一个用于测试的独立 Web 服务器。
**
** --loopback Only accept loop-back TCP connections (connections
** originating from the same host). This is the
** default if --root is omitted.
仅接受回环 TCP 连接。
**
** --family ipv4 Only accept input from IPV4 or IPV6, respectively.
** --family ipv6 These options are only meaningful if althttpd is run
** as a stand-alone server.
仅接受 IPV4 或 IPV6 输入。
**
** --jail BOOLEAN Indicates whether or not to form a chroot jail if
** initially run as root. The default is true, so the only
** useful variant of this option is "--jail 0" which prevents
** the formation of the chroot jail.
指示是否形成 chroot jail。
**
** --max-age SEC The value for "Cache-Control: max-age=%d". Defaults to
** 120 seconds.
"Cache-Control: max-age=%d" 的值,默认为 120 秒。
**
** --max-cpu SEC Maximum number of seconds of CPU time allowed per
** HTTP connection. Default 30 (build option:
** -DMAX_CPU=integer). 0 means no limit.
每个 HTTP 连接允许的最大 CPU 时间。
**
** --debug BOOLEAN Disables input timeouts. This is useful for debugging
** when inputs are being typed in manually.
禁用输入超时,用于调试。
**
** --enable-sab Add new lines to the HTTP reply header that are
** prerequisites for SharedArrayBuffer. These are the lines:
** Cross-Origin-Embedder-Policy: require-corp
** Cross-Origin-Opener-Policy: same-origin
为 SharedArrayBuffer 添加新行到 HTTP 回复头。
**
**
** Additional command-line options available when compiling with ENABLE_TLS:
**
** --cert FILE The TLS certificate, the "fullchain.pem" file
**
** --pkey FILE The TLS private key, the "privkey.pem" file. May be
** omitted if the --cert file is the concatenation of
** the fullchain.pem and the privkey.pem.
**
**
** Command-line options can take either one or two initial "-" characters.
** So "--debug" and "-debug" mean the same thing, for example.
**
**
** Security Features: 安全特性
**
** (1) This program automatically puts itself inside a chroot jail if
** it can and if not specifically prohibited by the "--jail 0"
** command-line option. The root of the jail is the directory that
** contains the various $HOST.website content subdirectories.
自动将自身置于 chroot jail 中。
**
** (2) No input is read while this process has root privileges. Root
** privileges are dropped prior to reading any input (but after entering
** the chroot jail, of course). If root privileges cannot be dropped
** (for example because the --user command-line option was omitted or
** because the user specified by the --user option does not exist),
** then the process aborts with an error prior to reading any input.
在具有 root 特权时不读取任何输入。
**
** (3) The length of an HTTP request is limited to MAX_CONTENT_LENGTH bytes
** (default: 250 million). Any HTTP request longer than this fails
** with an error. (Build option: -DMAX_CONTENT_LENGTH=integer)
限制 HTTP 请求长度。
**
** (4) There are hard-coded time-outs on each HTTP request. If this process
** waits longer than the timeout for the complete request, or for CGI
** to finish running, then this process aborts. (The timeout feature
** can be disabled using the --debug command-line option.)
对每个 HTTP 请求设置硬编码的超时。
**
** (5) If the HTTP_HOST request header contains characters other than
** [0-9a-zA-Z,-./:_~] then the entire request is rejected.
针对 HTTP_HOST 请求头进行字符检查。
**
** (6) Any characters in the URI pathname other than [0-9a-zA-Z,-./:_~]
** are converted into "_". This applies to the pathname only, not
** to the query parameters or fragment.
转换 URI 路径中的特殊字符。
**
** (7) If the first character of any URI pathname component is "." or "-"
** then a 404 Not Found reply is generated. This prevents attacks
** such as including ".." or "." directory elements in the pathname
** and allows placing files and directories in the content subdirectory
** that are invisible to all HTTP requests, by making the first
** character of the file or subdirectory name "-" or ".".
防止 URI 路径以 "." 或 "-" 开头。
**
** (8) The request URI must begin with "/" or else a 404 error is generated.
请求 URI 必须以 "/" 开头。
**
** (9) This program never sets the value of an environment variable to a
** string that begins with "() {".
**
** (10) If the --ipshun option is used, specific IP addresses can be
** temporarily block for abusive behavior.
不设置环境变量值以 "(){..." 开头。
**
** (11) If a CGI reports status code 418 ("I'm a teapot", rfc2324) and if
** the --ipshun option is used, then the IP address is temporarily
** blocked.
使用 --ipshun 选项可临时阻止特定 IP 地址。
**
** Security Auditing: 安全审计
**
** This webserver mostly only serves static content. Any security risk will
** come from CGI and SCGI. To check an installation for security, then, it
** makes sense to focus on the CGI and SCGI scripts.
此 Web 服务器主要用于提供静态内容。任何安全风险都将来自 CGI 和 SCGI。
因此,要检查安装的安全性,重点应放在 CGI 和 SCGI 脚本上。
**
** To locate all CGI files: 查找所有 CGI 文件的方法:
**
** find *.website -executable -type f -print
** OR: find *.website -perm +0111 -type f -print
**
** The first form of the "find" command is preferred, but is only supported
** by GNU find. On a Mac, you'll have to use the second form.
"find" 命令的第一种形式更受欢迎,但仅受 GNU find 支持。在 Mac 上,您必须使用第二种形式。
**
** To find all SCGI files: 查找所有 SCGI 文件的方法:
**
** find *.website -name '*.scgi' -type f -print
**
** If any file is a security concern, it can be disabled on a live
** installation by turning off read permissions:
如果任何文件引起安全担忧,可以通过关闭读取权限在生产环境中禁用该文件:
**
** chmod 0000 file-of-concern
**
** SCGI Specification Files: SCGI 规范文件:
**
** Content files (files without the execute bit set) that end with ".scgi"
** specify a connection to an SCGI server. The format of the .scgi file
** follows this template:
不包含执行位的内容文件(以 ".scgi" 结尾的文件)指定与 SCGI 服务器的连接。 .scgi 文件的格式如下:
**
** SCGI hostname port
** fallback: fallback-filename
** relight: relight-command
**
** The first line specifies the location and TCP/IP port of the SCGI
** server that will handle the request. Subsequent lines determine
** what to do if the SCGI server cannot be contacted. If the
** "relight:" line is present, then the relight-command is run using
** system() and the connection is retried after a 1-second delay. Use
** "&" at the end of the relight-command to run it in the background.
** Make sure the relight-command does not generate output, or that
** output will become part of the SCGI reply. Add a ">/dev/null"
** suffix (before the "&") to the relight-command if necessary to
** suppress output. If there is no relight-command, or if the relight
** is attempted but the SCGI server still cannot be contacted, then
** the content of the fallback-filename file is returned as a
** substitute for the SCGI request. The mimetype is determined by the
** suffix on the fallback-filename. The fallback-filename would
** typically be an error message indicating that the service is
** temporarily unavailable.
第一行指定 SCGI 服务器的位置和 TCP/IP 端口,随后的行确定如果无法联系 SCGI 服务器将采取什么措施。如果存在 "relight:" 行,则使用 system() 运行 relight-command,并在 1 秒延迟后重试连接。在 relight-command 的末尾使用 "&" 以在后台运行它。确保 relight-command 不会生成输出,否则输出将成为 SCGI 回复的一部分。如果没有 relight-command,或者尝试 relight 但 SCGI 服务器仍无法联系,则将 fallback-filename 文件的内容作为 SCGI 请求的替代返回。 mimetype 由 fallback-filename 的后缀确定。 fallback-filename 通常是指示服务暂时不可用的错误消息。
**
** Basic Authorization: 基本授权
**
** If the file "-auth" exists in the same directory as the content file
** (for both static content and CGI) then it contains the information used
** for basic authorization. The file format is as follows:
如果文件 "-auth" 存在于与内容文件(对于静态内容和 CGI 文件都是如此)相同的目录中,则其中包含用于基本授权的信息。文件格式如下:
**
** * Blank lines and lines that begin with '#' are ignored
** * "http-redirect" forces a redirect to HTTPS if not there already
** * "https-only" disallows operation in HTTP
** * "user NAME LOGIN:PASSWORD" checks to see if LOGIN:PASSWORD
** authorization credentials are provided, and if so sets the
** REMOTE_USER to NAME.
** * "realm TEXT" sets the realm to TEXT.
空白行和以 '#' 开头的行将被忽略。
"http-redirect" 强制将请求重定向到 HTTPS(如果尚未进行)。
"https-only" 不允许在 HTTP 中操作。
"user NAME LOGIN:PASSWORD" 检查是否提供了 LOGIN:PASSWORD 授权凭据,如果是,则将 REMOTE_USER 设置为 NAME。
"realm TEXT" 将 realm 设置为 TEXT。
**
** There can be multiple "user" lines.
If no "user" line matches, the request fails with a 401 error.
可以有多个 "user" 行。如果没有匹配的 "user" 行,则请求将以 401 错误失败。
** Because of security rule (7), there is no way for the content of the "-auth"
** file to leak out via HTTP request.
由于安全规则(7),不存在通过 HTTP 请求泄漏 "-auth" 文件内容的途径。
*/
头文件部分
#include <stdio.h> //printf、scanf、fopen ...
#include <ctype.h> //isalpha、isdigit
#include <syslog.h> //向系统日志提交信息
#include <stdlib.h> //内存分配和释放的函数,随机数生成
#include <sys/stat.h> //文件状态信息获取函数,如 stat、chmod
#include <unistd.h> //UNIX系统调用,如 fork、pipe、read、write
#include <fcntl.h> //文件描述符进行操作的函数,如 open、fcntl
#include <string.h> //字符串操作函数,如 strcpy、strcat、strlen
#include <pwd.h> //对用户账户信息的访问,如 getpwnam、getpwuid
#include <sys/time.h> //对时间和时间间隔的处理函数,如 gettimeofday
#include <sys/types.h> //系统调用和库函数使用的基本数据类型
#include <sys/resource.h> //对进程资源限制的控制函数,如 getrlimit、setrlimit
#include <sys/socket.h> //套接字编程的函数,如 socket、bind、listen
#include <sys/wait.h> //进程等待和状态获取函数,如 wait、waitpid
#include <netinet/in.h> //与网络相关的结构和常量,如 struct sockaddr_in
#include <arpa/inet.h> //网络地址转换函数,如 inet_pton、inet_ntop
#include <stdarg.h> //可变数量参数的函数,如 va_start、va_arg
#include <time.h> //时间和日期函数,如 time、strftime
#include <sys/times.h> //处理进程时间的函数,如 times
#include <netdb.h> //网络数据库(如 /etc/hosts)查询函数,如 gethostbyname
#include <errno.h> //全局变量 errno,用于存储最近一次发生的错误码
#include <sys/resource.h> //
#include <signal.h> //信号处理函数,如 signal、kill
#include <dirent.h> //目录操作函数,如 opendir、readdir
#ifdef linux
#include <sys/sendfile.h> //高效的文件拷贝函数 sendfile
#endif
#include <assert.h> //提供 assert 宏,用于检查程序运行时的条件
常量部分
/*
** Configure the server by setting the following macros and recompiling.
通过设置以下宏并重新编译来配置服务器。
*/
#ifndef DEFAULT_PORT
#define DEFAULT_PORT "80" /* Default TCP port for HTTP ,HTTP 的默认 TCP 端口 */
#endif
#ifndef MAX_CONTENT_LENGTH
#define MAX_CONTENT_LENGTH 250000000 /* Max length of HTTP request content,HTTP 请求内容的最大长度 */
#endif
#ifndef MAX_CPU
#define MAX_CPU 30 /* Max CPU cycles in seconds,CPU 循环的最大秒数 */
#endif
#ifndef ALTHTTPD_VERSION
#define ALTHTTPD_VERSION "2.0"
#endif
#ifndef BANISH_TIME
#define BANISH_TIME 300 /* How long to banish for abuse (sec) ,封禁滥用的时长(秒)*/
#endif
#ifndef SERVER_SOFTWARE
# define SERVER_SOFTWARE "althttpd " ALTHTTPD_VERSION
#endif
#ifndef SERVER_SOFTWARE_TLS
# ifdef ENABLE_TLS
# define SERVER_SOFTWARE_TLS SERVER_SOFTWARE ", " OPENSSL_VERSION_TEXT
# else
# define SERVER_SOFTWARE_TLS SERVER_SOFTWARE
# endif
#endif
/*
** Clock ID to use for clock_gettime(), for use in collecting the
** wall-clock processing time using clock_gettime() (which is
** signal-safe). We use clock_gettime(), rather than gmtime(), for
** measuring request wall-clock time because it's signal-safe.
** See discussion at:
** https://sqlite.org/althttpd/forumpost/4dc31619341ce947
用于 clock_gettime() 的时钟标识,用于使用 clock_gettime()(它是信号安全的)收集墙钟处理时间。
我们使用 clock_gettime() 而不是 gmtime() 来测量请求的墙钟时间,因为它是信号安全的。详见讨论:
*/
#ifdef _POSIX_MONOTONIC_CLOCK
# define ALTHTTPD_CLOCK_ID CLOCK_MONOTONIC
#else
# define ALTHTTPD_CLOCK_ID CLOCK_REALTIME
/* noting that this can jump if the system time changes
注意,如果系统时间更改,这可能会跳跃 */
#endif
程序数据集
/*
** We record most of the state information as global variables. This
** saves having to pass information to subroutines as parameters, and
** makes the executable smaller...
我们将大部分状态信息记录为全局变量。
这样可以避免将信息作为参数传递给子程序,也使得可执行文件更小...
*/
static const char *zRoot = 0; /* Root directory of the website 网站的根目录*/
static char *zPostData= 0; /* POST data POST请求的数据*/
static int nPostData = 0; /* Number of bytes of POST data POST数据的字节数*/
static char *zProtocol = 0; /* The protocol being using by the browser 浏览器使用的协议*/
static char *zMethod = 0; /* The method. Must be GET HTTP请求的方法,必须为GET*/
static char *zScript = 0; /* The object to retrieve 要获取的对象*/
static char *zRealScript = 0; /* The object to retrieve. Same as zScript except might have "/index.html" appended
要获取的对象,与zScript相同,可能附加了"/index.html"。*/
static char *zRequestUri = 0; /* Sanitized request uri 经过处理的请求URI。*/
static char *zHome = 0; /* The directory containing content 包含内容的目录。*/
static char *zQueryString = 0; /* The query string on the end of the name 名称末尾的查询字符串。 */
static char *zFile = 0; /* The filename of the object to retrieve 要获取的对象的文件名。*/
static int lenFile = 0; /* Length of the zFile name, zFile名称的长度。*/
static char *zDir = 0; /* Name of the directory holding zFile 包含zFile的目录的名称。*/
static char *zPathInfo = 0; /* Part of the pathname past the file 文件之后路径的一部分。*/
static char *zAgent = 0; /* What type if browser is making this query 发起此查询的浏览器类型。*/
static char *zServerName = 0; /* The name after the http:// "http://"后的名称。*/
static char *zServerPort = 0; /* The port number 端口号。*/
static char *zServerSoftware = 0;/* Software name and version info 软件名称和版本信息。*/
static char *zCookie = 0; /* Cookies reported with the request 请求中报告的Cookies。*/
static char *zHttpHost = 0; /* Name according to the web browser 根据Web浏览器的名称。*/
static char *zRealPort = 0; /* The real TCP port when running as daemon 作为守护进程运行时的实际TCP端口。*/
static char *zRemoteAddr = 0; /* IP address of the request 请求的IP地址。*/
static char *zReferer = 0; /* Name of the page that refered to us 引用我们的页面的名称。*/
static char *zAccept = 0; /* What formats will be accepted 将接受的格式。*/
static char *zAcceptEncoding =0; /* gzip or default 接受的编码格式,可以是gzip或默认。*/
static char *zContentLength = 0; /* Content length reported in the header 报头中报告的内容长度。*/
static char *zContentType = 0; /* Content type reported in the header 报头中报告的内容类型。*/
static char *zQuerySuffix = 0; /* The part of the URL after the first ? 第一个问号之后的URL的一部分。*/
static char *zAuthType = 0; /* Authorization type (basic or digest) 授权类型(基本或摘要)。*/
static char *zAuthArg = 0; /* Authorization values 授权值。*/
static char *zRemoteUser = 0; /* REMOTE_USER set by authorization module 由授权模块设置的REMOTE_USER。 */
static char *zIfNoneMatch= 0; /* The If-None-Match header value 表示If-None-Match头的值。*/
static char *zIfModifiedSince=0; /* The If-Modified-Since header value 表示If-Modified-Since头的值。*/
static char *zHttpScheme = "http";/* HTTP_SCHEME CGI variable 表示HTTP_SCHEME CGI变量,默认为"http"。*/
static char *zHttps = 0; /* HTTPS CGI variable 表示HTTPS CGI变量。*/
static int nIn = 0; /* Number of bytes of input 输入的字节数。*/
static int nOut = 0; /* Number of bytes of output 输出的字节数。*/
static char zReplyStatus[4]; /* Reply status code 回复的状态码。*/
static int statusSent = 0; /* True after status line is sent 在发送状态行后为真。*/
static const char *zLogFile = 0; /* Log to this file 记录日志到这个文件。*/
static char zExpLogFile[500] = {0}; /* %-expanded log file name 表示%扩展的日志文件名。*/
static const char *zIPShunDir=0; /* Directory containing hostile IP addresses 包含敌对IP地址的目录。*/
static int debugFlag = 0; /* True if being debugged如果正在调试,则为真。 */
static struct timeval beginTime; /* Time when this process starts 进程启动的时间。*/
static struct timespec tsBeginTime; /* clock_gettime() when request processing starts 请求处理开始时的clock_gettime()。*/
static int closeConnection = 0; /* True to send Connection: close in reply 为真时在回复中发送Connection: close。*/
static int nRequest = 0; /* Number of requests processed 处理的请求数量。*/
static int omitLog = 0; /* Do not make logfile entries if true 如果为真,则不生成日志文件条目。*/
static int useHttps = 0; /* 0=HTTP, 1=external HTTPS (stunnel), 2=builtin TLS support
使用的协议,0表示HTTP,1表示外部HTTPS(通过stunnel),2表示内置的TLS支持。*/
static int useTimeout = 1; /* True to use times 为真时使用时间。*/
static int nTimeoutLine = 0; /* Line number where timeout was set 设置超时的行号。 */
static int standalone = 0; /* Run as a standalone server (no inetd)为真时作为独立服务器运行(无inetd)。 */
static int ipv6Only = 0; /* Use IPv6 only */
static int ipv4Only = 0; /* Use IPv4 only */
static struct rusage priorSelf; /* Previously report SELF time 表示之前报告的SELF时间。*/
static struct rusage priorChild; /* Previously report CHILD time 之前报告的CHILD时间。*/
/*static struct timespec tsSelf;*/
static int mxAge = 120; /* Cache-control max-age 表示Cache-control的最大年龄。*/
static char *default_path = "/bin:/usr/bin"; /* Default PATH variable 默认的PATH变量。*/
static char *zScgi = 0; /* Value of the SCGI env variable 表示SCGI环境变量的值*/
static int rangeStart = 0; /* Start of a Range: request 表示Range请求的起始位置。*/
static int rangeEnd = 0; /* End of a Range: request 表示Range请求的结束位置。*/
static int maxCpu = MAX_CPU; /* Maximum CPU time per process 每个进程的最大CPU时间。 */
static int enableSAB = 0; /* Add reply header to enable SharedArrayBuffer
是否添加响应头以启用SharedArrayBuffer。
SharedArrayBuffer是一种在Web Workers之间共享数据的机制。*/
static int inSignalHandler = 0; /* True if running a signal handler 是否正在运行信号处理程序。*/
cgienv CGI环境变量
CGI(Common Gateway Interface)环境变量是在 Web 服务器和 CGI 程序之间传递信息的一种标准机制。当客户端发起 HTTP 请求时,Web 服务器会将一组特定的环境变量传递给 CGI 程序,以提供有关请求和运行环境的信息。这些环境变量由 Web 服务器设置,而 CGI 程序可以读取和利用这些变量来执行相应的操作。
/* Mapping between CGI variable names and values stored in global variables.
映射 CGI 变量名和存储在全局变量中的值 */
static struct {
char *zEnvName; // CGI 环境变量的名称
char **pzEnvValue; //CGI 环境变量的值
} cgienv[] = {
{ "CONTENT_LENGTH", &zContentLength }, /* Must be first for SCGI */
{ "AUTH_TYPE", &zAuthType },
{ "AUTH_CONTENT", &zAuthArg },
{ "CONTENT_TYPE", &zContentType },
{ "DOCUMENT_ROOT", &zHome },
{ "HTTP_ACCEPT", &zAccept },
{ "HTTP_ACCEPT_ENCODING", &zAcceptEncoding },
{ "HTTP_COOKIE", &zCookie },
{ "HTTP_HOST", &zHttpHost },
{ "HTTP_IF_MODIFIED_SINCE", &zIfModifiedSince },
{ "HTTP_IF_NONE_MATCH", &zIfNoneMatch },
{ "HTTP_REFERER", &zReferer },
{ "HTTP_SCHEME", &zHttpScheme },
{ "HTTP_USER_AGENT", &zAgent },
{ "HTTPS", &zHttps },
{ "PATH", &default_path },
{ "PATH_INFO", &zPathInfo },
{ "QUERY_STRING", &zQueryString },
{ "REMOTE_ADDR", &zRemoteAddr },
{ "REQUEST_METHOD", &zMethod },
{ "REQUEST_URI", &zRequestUri },
{ "REMOTE_USER", &zRemoteUser },
{ "SCGI", &zScgi },
{ "SCRIPT_DIRECTORY", &zDir },
{ "SCRIPT_FILENAME", &zFile },
{ "SCRIPT_NAME", &zRealScript },
{ "SERVER_NAME", &zServerName },
{ "SERVER_PORT", &zServerPort },
{ "SERVER_PROTOCOL", &zProtocol },
{ "SERVER_SOFTWARE", &zServerSoftware },
};
althttpd_printf、althttpd_vprintf 打印信息
/*
** A printf() proxy which outputs either to stdout or the outbound TLS
** connection, depending on connection state. It uses a
** statically-sized buffer for TLS output and will fail (via
** Malfunction()) if it's passed too much data. In non-TLS mode it has
** no such limitation. The buffer is generously sized, in any case, to
** be able to handle all of the headers output by althttpd as of the
** time of this writing.
** althttpd_vprintf 函数是 printf() 函数的代理,根据连接状态输出到 stdout 或者 TLS 连接。
** 在 TLS 模式下,它使用一个静态大小的缓冲区进行 TLS 输出,并且如果传递了过多的数据,
** 它将通过 Malfunction() 报错。在非 TLS 模式下,它没有这种限制。
** 无论如何,缓冲区的大小足够大,以处理截至代码编写时 althttpd 输出的所有头部。
*/
#ifdef ENABLE_TLS //TLS (Transport Layer Security)
static int althttpd_vprintf(char const * fmt, va_list va){
if( useHttps!=2 || NULL==tlsState.sslCon ){
// 如果不是 TLS 模式或者 sslCon 为空,则调用 vprintf() 直接输出到 stdout
return vprintf(fmt, va);
}else{
char pfBuffer[10000];
// 使用 vsnprintf 将格式化的字符串写入 pfBuffer 中,不超过 pfBuffer 大小
const int sz = vsnprintf(pfBuffer, sizeof(pfBuffer), fmt, va);
if( sz<(int)sizeof(pfBuffer) ){
// 如果写入的大小小于 pfBuffer 的大小,则通过 tls_write_server 输出到 TLS 连接
return (int)tls_write_server(tlsState.sslCon, pfBuffer, sz);
}else{
// 如果写入大小超过 pfBuffer 的大小,通过 Malfunction() 报错
Malfunction(529, /* LOG: Output buffer too small */
"Output buffer is too small. Wanted %d bytes.", sz);
return 0;
}
}
}
#else
#define althttpd_vprintf vprintf
#endif
#ifdef ENABLE_TLS
static int althttpd_printf(char const * fmt, ...){
int rc;
va_list va;
va_start(va,fmt);
rc = althttpd_vprintf(fmt, va);
va_end(va);
return rc;
}
static void *tls_new_server(int iSocket);
static void tls_close_server(void *pServerArg);
static void tls_atexit(void);
#else
#define althttpd_printf printf
#endif
time 转换
/* A structure for holding a single date and time. 用于保存单个日期和时间的结构体。 */
typedef struct DateTime DateTime;
struct DateTime {
long long iJD; /* The julian day number times 86400000,朱利安日数乘以 86400000 */
int Y, M, D; /* Year, month, and day */
int h, m; /* Hour and minutes */
double s; /* Seconds */
};
/* Populates p based on the time stored in t. 根据存储在 t 中的时间填充 p。 */
static void unixToDateTime(time_t t, DateTime *p){
int Z, A, B, C, D, E, X1;
int day_s;
p->iJD = (long long)((t/86400.0 + 2440587.5) * 86400000);
Z = (int)((p->iJD + 43200000)/86400000);
A = (int)((Z - 1867216.25)/36524.25);
A = Z + 1 + A - (A/4);
B = A + 1524;
C = (int)((B - 122.1)/365.25);
D = (36525*(C&32767))/100;
E = (int)((B-D)/30.6001);
X1 = (int)(30.6001*E);
p->D = B - D - X1;
p->M = E<14 ? E-1 : E-13;
p->Y = p->M>2 ? C - 4716 : C - 4715;
day_s = t % 86400;
p->h = day_s / 3600;
p->m = day_s % 3600 / 60;
p->s = day_s % 60;
#if 0
fprintf(stderr,"day_s = %d, DateTime Y=%d M=%d D=%d h=%d m=%d s=%d\n",
(int)day_s,
p->Y, p->M, p->D,
p->h, p->m, (int)p->s);
#endif
}
#ifdef COMBINED_LOG_FORMAT
/*
** Copies and converts _relevant_ fields of pD into pTm. Only those
** fields relevant for althttpd's own logging are handled and the rest
** are zeroed out, thus pTm is not necessarily suitable for all
** strftime() formats or libc time functions after calling this.
** 将 pD 的相关字段复制并转换为 pTm。仅处理 althttpd 自身日志记录所需的字段,
** 其余字段将被置零,因此调用此函数后,pTm 不一定适用于所有 strftime() 格式
** 或 libc 时间函数。
*/
static void DateTime_toTm(const DateTime *pD, struct tm *pTm){
memset(pTm, 0, sizeof(*pTm));
pTm->tm_isdst = -1;
pTm->tm_sec = (int)pD->s;
pTm->tm_min = pD->m;
pTm->tm_hour = pD->h;
pTm->tm_mday = pD->D;
pTm->tm_mon = pD->M - 1;
pTm->tm_year = pD->Y - 1900;
}
#endif
/* Convert a struct timeval into an integer number of microseconds
将 struct timeval 转换为微秒数。 */
static long long int tvms(struct timeval *p){
return ((long long int)p->tv_sec)*1000000 + (long long int)p->tv_usec;
}
/* Convert a struct timespec into an integer number of microseconds
struct timespec 转换为微秒数。 */
static long long int tsms(struct timespec *p){
return ((long long int)p->tv_sec)*1000000 + (long long int)(p->tv_nsec/1000);
}
log 日志
/*
** If (z+strlen(zIn)) < zEnd then this appends zIn to z, sets *zTail
** to the end of that string, and returns z. If (z+strlen(zIn)) >= zEnd
** then *zTail is set to 0 and 0 is returned. If z is NULL, *zTail
** is set to 0 but this is otherwise a no-op.
** 如果 (z+strlen(zIn)) < zEnd,则将 zIn 追加到 z 中,将 *zTail 设置为该字符串的末尾,并返回 z。
** 如果 (z+strlen(zIn)) >= zEnd,则将 *zTail 设置为 0 并返回 0。如果 z 为 NULL,则将 *zTail 设置为 0,但这是一个无操作。
*/
static char * log_str(char *z, const char *zEnd, const char *zIn, char **zTail){
const size_t n = z ? strlen(zIn) : 0;
if( !z || zEnd<=z+n ){
*zTail = 0;
return 0;
}
memcpy( z, zIn, n );
*zTail = z+n;
return z;
}
/*
** Works like log_str() but doubles all double-quote characters in the
** intput. If z is NULL or zIn is too long for the input buffer,
** *zTail is set to 0 and 0 is returned.
** This is used to quote strings for output into the CSV log file.
** 与 log_str() 类似,但是将输入中的所有双引号字符加倍。如果 z 为 NULL 或者 zIn 对于输入缓冲区来说太长,
** 则将 *zTail 设置为 0 并返回 0。
** 这用于为输出到 CSV 日志文件的字符串进行引用。
*/
static char * log_escstr(char *z, const char *zEnd, const char *zIn,
char **zTail){
char * zOrig = z;
char c = *zIn;
if( !z || z>=zEnd ){
goto too_short;
}
for( ; c && z<zEnd; c=*++zIn ){
if( '"'==c ){
if( z>=zEnd-1 ){
goto too_short;
}
*z++ = c;
}
*z++ = c;
}
*zTail = z;
return zOrig;
too_short:
*zTail = 0;
return 0;
}
/*
** Appends d to z, sets *zTail to the byte after the new end of z, and
** returns z. If appending d would extend past zEnd then *zTail is set
** to 0 and 0 is returned, but z is not modified. If z is NULL, *zTail
** is set to 0 but this is otherwise a no-op.
**
** minDigits must be at least 1 and d will be left-padded with 0's to
** a minimum of that length.
** 将 d 追加到 z,将 *zTail 设置为新的 z 末尾的字节,并返回 z。
** 如果追加 d 会超过 zEnd,则将 *zTail 设置为 0 并返回 0,但 z 不会被修改。
** 如果 z 为 NULL,则将 *zTail 设置为 0,但这是一个无操作。
** minDigits 必须至少为 1,并且 d 将用 0 填充到至少该长度。
*/
static char * log_int(char *z, const char *zEnd, long long d,
int minDigits, char **zTail){
char buf[128];
int i = (int)sizeof(buf), n = 0;
int neg = 0;
assert( minDigits>0 );
if( !z ){
*zTail = 0;
return 0;
}else if( d<0 ){
neg = 1;
d = -d;
}
for( ; (minDigits-- > 0) || d>0; d/=10 ){
buf[--i] = '0' + (d%10);
assert(i>0);
++n;
}
if( neg ){
buf[--i] = '-';
++n;
}
if( zEnd <= z+n ){
return 0;
}
memcpy(z, &buf[i], n);
*zTail = z+n;
return z;
}
#ifndef COMBINED_LOG_FORMAT
/*
** A proxy for log_str() and log_int() which appends 19 bytes of
** pD's timestamp to z in the form "YYYY-MM-DD hh:mm:ss", assigns
** *zTail to the address one after the end of that string, and returns
** z. If (z+19 >= zEnd) then *zTail is set to 0 and 0 is returned, but
** this is otherwise a no-op.
** log_str() 和 log_int() 的代理,将 pD 的时间戳的 19 个字节追加到 z 中,格式为 "YYYY-MM-DD hh:mm:ss",
** 将 *zTail 分配给该字符串的结尾地址,并返回 z。
** 如果 (z+19 >= zEnd) 则将 *zTail 设置为 0 并返回 0,但这是一个无操作。
*/
static char * log_DateTime(char * z, const char *zEnd,
const struct DateTime *pD, char **zTail){
char * zOut = z;
if( z+19 >= zEnd ){
*zTail = 0;
return 0;
}
#define aint(N,DIGITS,SUFFIX) log_int( zOut, zEnd, (int)(N), (DIGITS), &zOut ); \
if( SUFFIX ) log_str( zOut, zEnd, SUFFIX, &zOut )
aint(pD->Y, 4, "-");
aint(pD->M, 2, "-");
aint(pD->D, 2, " ");
aint(pD->h, 2, ":");
aint(pD->m, 2, ":");
aint(pD->s, 2, 0);
#undef aint
*zTail = zOut;
return z;
}
#endif
/*
** Make an entry in the log file. If the HTTP connection should be
** closed, then terminate this process. Otherwise return.
** 在日志文件中创建一个条目。如果 HTTP 连接应该关闭,那么终止该进程。否则返回。
*/
static void MakeLogEntry(int exitCode, int lineNum){
int logfd;
if( zPostData ){// 如果存在 zPostData,则释放它
if( inSignalHandler==0 ){
free(zPostData);
}
zPostData = 0;
}
if( zLogFile && !omitLog ){// 如果需要记录日志且未被忽略,则进行日志记录
struct rusage self, children;
int waitStatus;
const char *zRM = zRemoteUser ? zRemoteUser : "";
const char *zFilename;
// 设置一些可能为空的字符串
if( zScript==0 ) zScript = "";
if( zRealScript==0 ) zRealScript = "";
if( zRemoteAddr==0 ) zRemoteAddr = "";
if( zHttpHost==0 ) zHttpHost = "";
if( zReferer==0 ) zReferer = "";
if( zAgent==0 ) zAgent = "";
if( zQuerySuffix==0 ) zQuerySuffix = "";
// 确定日志文件名
if( zExpLogFile[0]!=0 ){
zFilename = zExpLogFile;
}else{
zFilename = zLogFile;
}
waitpid(-1, &waitStatus, WNOHANG);// 在非阻塞方式下等待子进程的退出
// 如果在信号处理程序中,使用 getrusage() 时存在不安全性,因此将其置零
if( inSignalHandler!=0 ){
/* getrusage() is not signal-safe, so fall back to 0.
不是信号安全的,因此回退到 0。*/
memset(&self, 0, sizeof(self));
memset(&children, 0, sizeof(children));
}else{// 获取自身和子进程的资源使用情况
getrusage(RUSAGE_SELF, &self);
getrusage(RUSAGE_CHILDREN, &children);
}
// 打开日志文件,以追加、创建和写入方式,权限为 0640
if( (logfd = open(zFilename,O_WRONLY | O_CREAT | O_APPEND, 0640)) > 0 ){
time_t tNow; /* current time 当前时间 */
struct timespec tsNow; /* current time (again! 再次获取) */
struct DateTime dt; /* high-level tNow 高级的 tNow */
char msgbuf[5000]; /* message buffer 消息缓冲区*/
const char * const zEnd = &msgbuf[0] + sizeof(msgbuf);
char * zPos = &msgbuf[0]; /* current write pos 当前写入位置 */
#define astr( STR ) log_str( zPos, zEnd, (STR), &zPos )
#define acomma astr(",")
#define astr2( STR ) astr( STR ); acomma
#define aint( N ) log_int( zPos, zEnd, (N), 1, &zPos ); acomma
// 获取当前时间和时钟时间
clock_gettime(ALTHTTPD_CLOCK_ID, &tsNow);
time(&tNow);
unixToDateTime(tNow, &dt);
#ifdef COMBINED_LOG_FORMAT // 如果使用 COMBINED_LOG_FORMAT,则记录日志的格式会稍有不同
/* Potential TODO: eliminate use of strftime(). Default builds
** of althttpd do not use COMBINED_LOG_FORMAT, so this is a low-priority issue. */
/* 潜在的 TODO: 消除对 strftime() 的使用。
默认构建althttpd 不使用 COMBINED_LOG_FORMAT,因此这是一个低优先级的问题。*/
{
struct tm vTm/* to remove once we have a strftime() substitute */;//一旦我们有了 strftime() 替代品,就可以删除
char zDate[200];
#if 0
/* Older impl, for reference: 旧实现,供参考: */
fprintf(log, "%s - - [%s] \"%s %s %s\" %s %d \"%s\" \"%s\"\n",
zRemoteAddr, zDate, zMethod, zScript, zProtocol,
zReplyStatus, nOut, zReferer, zAgent);
#endif
DateTime_toTm(&dt, &vTm);
strftime(zDate, sizeof(zDate), "%d/%b/%Y:%H:%M:%S %Z", &vTm);
astr( zRemoteAddr );
astr( " - - [" );
astr( zDate );
astr( "] \"" );
astr( zMethod );
astr( " " );
astr( zScript );
astr( " " );
astr( zProtocol );
astr( "\" " );
astr( zReplyStatus );
astr( " " );
aint( nOut );
astr( " \"" );
astr( zReferer );
astr( "\" \"" );
astr( zAgent );
astr( "\"\n" );
}
#else // 否则,按照其他格式记录日志
/* Log record files:
** (1) Date and time 日期和时间
** (2) IP address
** (3) URL being accessed 正在访问的 URL
** (4) Referer 引用者
** (5) Reply status 响应状态
** (6) Bytes received 接收到的字节数
** (7) Bytes sent
** (8) Self user time 自身用户时间
** (9) Self system time 自身系统时间
** (10) Children user time 子进程用户时间
** (11) Children system time 子进程系统时间
** (12) Total wall-clock time 总墙钟时间
** (13) Request number for same TCP/IP connection 相同 TCP/IP 连接的请求编号
** (14) User agent 用户代理
** (15) Remote user 远程用户
** (16) Bytes of URL that correspond to the SCRIPT_NAME 与 SCRIPT_NAME 相对应的 URL 字节数
** (17) Line number in source file 源文件中的行号
*/
#define escstr(X) log_escstr( zPos, zEnd, X, &zPos )
/* (1) */ log_DateTime( zPos, zEnd, &dt, &zPos );
astr(",");
/* (2) */ astr2( zRemoteAddr );
/* (3) */ astr( "\"" );
astr( zHttpScheme );
astr( "://" );
escstr(zHttpHost);
escstr(zScript);
escstr(zQuerySuffix);
astr2( "\"" );
/* (4) */ astr( "\"" ); escstr(zReferer); astr2("\"");
/* (5) */ astr2( zReplyStatus );
/* (6) */ aint( nIn );
/* (7) */ aint( nOut );
/* (8) */ aint( tvms(&self.ru_utime) - tvms(&priorSelf.ru_utime) );
/* (9) */ aint( tvms(&self.ru_stime) - tvms(&priorSelf.ru_stime) );
/* (10) */ aint( tvms(&children.ru_utime) - tvms(&priorChild.ru_utime) );
/* (11) */ aint( tvms(&children.ru_stime) - tvms(&priorChild.ru_stime) );
/* (12) */ aint( tsms(&tsNow) - tsms(&tsBeginTime) );
/* (13) */ aint( nRequest );
/* (14) */ astr( "\"" ); escstr(zAgent); astr2("\"");
/* (15) */ astr( "\"" ); escstr(zRM); astr2("\"");
/* (16) */ aint( strlen(zHttpScheme)+strlen(zHttpHost)+strlen(zRealScript)+3 );
/* (17) */ log_int( zPos, zEnd, lineNum, 1, &zPos );
#undef escstr
#ifdef ALTHTTPD_LOG_PID
/* Appending of PID to the log is used only to assist in debugging of hanging althttpd processes: 将 PID 附加到日志仅用于协助调试挂起的 althttpd 进程:
** https://sqlite.org/althttpd/forumpost/4dc31619341ce947 */
acomma;
/* (18) */ log_int( zPos, zEnd, getpid(), 1, &zPos );
#endif
astr( "\n" );
priorSelf = self;
priorChild = children;
#endif
if( zPos!=0 ){
size_t iStart = 0;
size_t toSend;
size_t nSent;
assert( zPos<zEnd-1 );
assert( zPos>&msgbuf[0] );
*zPos = 0;
toSend = zPos - &msgbuf[0];
while( 1 /*exit-by-break*/ ){
nSent = write( logfd, msgbuf+iStart, toSend);
if( nSent<=0 ) break;
if( nSent>=toSend ) break;
iStart += nSent;
toSend -= nSent;
}
}
close(logfd);
nIn = nOut = 0;
#undef astr
#undef astr2
#undef aint
#undef acomma
}
}
if( closeConnection || inSignalHandler ){
exit(exitCode);// 如果需要关闭连接或在信号处理程序中,则终止进程
}
statusSent = 0;
}
Malfunction 故障
用于在web服务器中通知客户端服务器发生了故障。
/* Forward reference 前向引用*/
static void Malfunction(int errNo, const char *zFormat, ...);
/* Print the first line of a response followed by the server type.
用于开始构建 HTTP 响应的第一行 */
static void StartResponse(const char *zResultCode){
time_t now;
time(&now);
if( statusSent ) return; 如果状态已发送,则直接返回,避免重复发送
打印 HTTP 响应的第一行,格式为 "HTTP-Version Status-Code",例如 "HTTP/1.1 200 OK"
nOut += althttpd_printf("%s %s\r\n",
zProtocol ? zProtocol : "HTTP/1.1",
zResultCode);
strncpy(zReplyStatus, zResultCode, 3); 将响应状态码的前三个字符复制到 zReplyStatus
zReplyStatus[3] = 0;
如果响应状态码的第一个字符大于等于 '4',则表示客户端错误或服务器错误,设置 closeConnection 为 1
if( zReplyStatus[0]>='4' ){
closeConnection = 1;
}
if( closeConnection ){
// 如果 closeConnection 为真,表示要关闭连接
nOut += althttpd_printf("Connection: close\r\n");
}else{
// 否则,表示保持连接,打印 "Connection: keep-alive"
nOut += althttpd_printf("Connection: keep-alive\r\n");
}
nOut += DateTag("Date", now);// 打印当前时间的 Date 标签
statusSent = 1;// 设置状态已发送标志为真
}
/* Tell the client that there is an error in the script.
告诉客户端脚本中存在错误。*/
static void CgiScriptWritable(void){
StartResponse("500 CGI Configuration Error");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"The CGI program %s is writable by users other than its owner.\n",
zRealScript);
MakeLogEntry(0, 140); /* LOG: CGI script is writable */
althttpd_exit();
}
/* Tell the client that the server malfunctioned.
通知客户端服务器发生了故障 */
void Malfunction(int linenum, const char *zFormat, ...){
va_list ap;
va_start(ap, zFormat);
StartResponse("500 Server Malfunction");//向客户端发送响应头,表示服务器故障(500 Server Malfunction)
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"Web server malfunctioned; error number %d\n\n", linenum);
if( zFormat ){ 如果有额外的格式化字符串(zFormat),将其格式化后发送给客户端
nOut += althttpd_vprintf(zFormat, ap);
althttpd_printf("\n");
nOut++;
}
va_end(ap);
MakeLogEntry(0, linenum);
althttpd_exit();
}
malloc with log
/* Allocate memory safely */
static char *SafeMalloc( size_t size ){
char *p;
p = (char*)malloc(size);
if( p==0 ){
strcpy(zReplyStatus, "998");
MakeLogEntry(1,100); /* LOG: Malloc() failed 主要是保证有个输出,方便调查问题*/
exit(1);
}
return p;
}
TLS:tls_gets,tls_read_server,tls_write_server
使用 OpenSSL 库实现 TLS(Transport Layer Security)功能的一部分。
使用 OpenSSL 库来处理加密和解密通信。
OpenSSL 库(Open Source implementation of the SSL and TLS protocols)
#ifdef ENABLE_TLS
#include <openssl/bio.h> //BIO(Basic Input/Output) 文件、内存、套接字等数据源和目标之间进行灵活的数据传输
#include <openssl/ssl.h> //SSL(Secure Sockets Layer)用于安全地传输数据。
#include <openssl/err.h> //处理错误和异常的 OpenSSL 函数和宏
#include <openssl/x509.h> //处理数字证书的创建、解析和验证
typedef struct TlsServerConn {
SSL *ssl; /* The SSL codec , SSL 编解码器*/
BIO *bio; /* SSL BIO object ,SSL BIO 对象 */
int iSocket; /* The socket, 套接字 */
} TlsServerConn;// 定义 TLS 服务器连接的结构体
/*
** There can only be a single OpenSSL IO connection open at a time.
** State information about that IO is stored in the following global singleton:
// 全局变量,用于存储唯一的 OpenSSL IO 连接的状态信息
*/
static struct TlsState {
int isInit; /* 0: uninit 1: init as client 2: init as server */
SSL_CTX *ctx;
const char *zCertFile; /* --cert CLI arg, --cert 命令行参数*/
const char *zKeyFile; /* --pkey CLI arg, --pkey 命令行参数*/
TlsServerConn * sslCon;
} tlsState = {
0, /* isInit */
NULL, /* SSL_CTX *ctx */
NULL, /* zCertFile */
NULL, /* zKeyFile */
NULL /* sslCon */
};
/*
** Read a single line of text from the client and stores it in zBuf
** (which must be at least nBuf bytes long). On error it
** calls Malfunction().
** If it reads anything, it returns zBuf.
// 从客户端读取一行文本并将其存储在 zBuf 中
*/
static char *tls_gets(void *pServerArg, char *zBuf, int nBuf){
int n = 0, err = 0;
int i;
TlsServerConn * const pServer = (TlsServerConn*)pServerArg;
if( BIO_eof(pServer->bio) ) return 0;// 如果在 BIO 中已经到达末尾,则返回 0
for(i=0; i<nBuf-1; i++){
n = SSL_read(pServer->ssl, &zBuf[i], 1);// 从 SSL 连接中读取一个字节
err = SSL_get_error(pServer->ssl, n);
if( err!=0 ){
Malfunction(525,"SSL read error.");// 如果出现错误,则调用 Malfunction() 函数
}else if( 0==n || zBuf[i]=='\n' ){
break;
}
}
zBuf[i+1] = 0;// 在末尾添加 null 字符
return zBuf;
}
/*
** Reads up tp nBuf bytes of TLS-decoded bytes from the client and
** stores them in zBuf, which must be least nBuf bytes long. Returns
** the number of bytes read. Fails fatally if nBuf is "too big" or if
** SSL_read() fails. Once pServerArg reaches EOF, this function simply
** returns 0 with no side effects.
// 从客户端读取最多 nBuf 字节的 TLS 解码字节
*/
static size_t tls_read_server(void *pServerArg, void *zBuf, size_t nBuf){
int err = 0;
size_t rc = 0;
TlsServerConn * const pServer = (TlsServerConn*)pServerArg;
if( nBuf>0x7fffffff ){
// 如果 nBuf 大于 0x7fffffff,则调用 Malfunction() 函数,记录错误并返回
Malfunction(526,"SSL read too big"); /* LOG: SSL read too big */
}
while( 0==err && nBuf!=rc && 0==BIO_eof(pServer->bio) ){
const int n = SSL_read(pServer->ssl, zBuf + rc, (int)(nBuf - rc));
if( n==0 ){// 如果读取到末尾,退出循环
break;
}
err = SSL_get_error(pServer->ssl, n);// 获取 SSL 错误码
if(0==err){
rc += n;
}else{// 如果出现错误,则调用 Malfunction() 函数
Malfunction(527,"SSL read error."); /* LOG: SSL read error */
}
}
return rc;
}
/*
** Write cleartext bytes into the SSL server codec so that they can
** be encrypted and sent back to the client. On success, returns
** the number of bytes written, else returns a negative value.
// 将明文字节写入 SSL 服务器编解码器,以便加密并发送回客户端
*/
static int tls_write_server(void *pServerArg, void const *zBuf, size_t nBuf){
int n;
TlsServerConn * const pServer = (TlsServerConn*)pServerArg;
if( nBuf<=0 ) return 0;// 如果 nBuf 小于等于 0,则返回 0
if( nBuf>0x7fffffff ){// 如果 nBuf 大于 0x7fffffff,则调用 Malfunction() 函数,记录错误并返回
Malfunction(528,"SSL write too big"); /* LOG: SSL write too big */
}
n = SSL_write(pServer->ssl, zBuf, (int)nBuf);// 调用 SSL_write() 函数将数据写入 SSL 连接
if( n<=0 ){
/* Do NOT call Malfunction() from here, as Malfunction()
** may output via this function. The current error handling
** is somewhat unsatisfactory, as it can lead to negative
** response length sizes in the althttpd log. */
/* 不要从这里调用 Malfunction(),
因为 Malfunction()可能通过此函数输出。当前的错误处理有些令人不满意,
因为它可能导致 althttpd 日志中出现负响应长度。*/
return -SSL_get_error(pServer->ssl, n);
}else{
return n;
}
}
#endif /* ENABLE_TLS */
tls_new_server,tls_close_server,tls_atexit
#ifdef ENABLE_TLS
/*
** Create a new server-side codec. The argument is the socket's file
** descriptor from which the codec reads and writes. The returned
** memory must eventually be passed to tls_close_server().
该函数用于创建一个新的服务器端编解码器。
接受一个套接字文件描述符作为参数,从该套接字读取和写入数据。
返回的内存必须最终传递给 tls_close_server 函数以释放资源。
*/
static void *tls_new_server(int iSocket){
TlsServerConn *pServer = malloc(sizeof(*pServer));
BIO *b = pServer ? BIO_new_socket(iSocket, 0) : NULL;
if( NULL==b ){
Malfunction(507,"Cannot allocate TlsServerConn."); /* LOG: TlsServerConn */
}
assert(NULL!=tlsState.ctx);
pServer->ssl = SSL_new(tlsState.ctx);
pServer->bio = b;
pServer->iSocket = iSocket;
SSL_set_bio(pServer->ssl, b, b);
SSL_accept(pServer->ssl);//开始进行 SSL 握手过程,即服务器端与客户端之间的协商和验证。
return (void*)pServer;
}
/*
** Close a server-side code previously returned from tls_new_server().
关闭先前由 tls_new_server 创建的服务器端编解码器。
*/
static void tls_close_server(void *pServerArg){
TlsServerConn *pServer = (TlsServerConn*)pServerArg;
SSL_free(pServer->ssl);
memset(pServer, 0, sizeof(TlsServerConn));
free(pServer);
}
static void tls_atexit(void){//在程序退出时执行
#if 0
/*
** Shutting down TLS can lead to spurious hung processes on some platforms/builds.
** See the long discussion on this at:
** https://sqlite.org/althttpd/forumpost/4dc31619341ce947
当前的实现被注释掉了,因为在某些平台和构建配置下,关闭 TLS 可能导致进程悬挂。
详细讨论可以参考链接中的注释。
*/
if( inSignalHandler==0 && tlsState.sslCon!=0 ){
tls_close_server(tlsState.sslCon);
tlsState.sslCon = NULL;
}
#endif
}
#endif /* ENABLE_TLS */
tls_init_conn,tls_close_conn,althttpd_fflush
在内置 TLS 模式下初始化 SSL I/O 状态以及关闭 TLS 连接
/*
** If running in builtin TLS mode, initializes the SSL I/O
** state and returns 1, else does nothing and returns 0.
*/
static int tls_init_conn(int iSocket){
#ifdef ENABLE_TLS
if( 2==useHttps ){
/*assert(NULL==tlsState.sslCon);*/
if( NULL==tlsState.sslCon ){
tlsState.sslCon = (TlsServerConn *)tls_new_server(iSocket);
if( NULL==tlsState.sslCon ){
Malfunction(512, /* LOG: TLS context */ "Could not instantiate TLS context.");
}
atexit(tls_atexit);//注册 tls_atexit 函数,以确保在程序退出时关闭 TLS 连接。
}
return 1;//返回 1 表示成功初始化 TLS 连接。
}
#else
if( 0==iSocket ){/*unused arg*/}
#endif
return 0;
}
static void tls_close_conn(void){
#ifdef ENABLE_TLS
if( tlsState.sslCon ){
tls_close_server(tlsState.sslCon);//关闭 TLS 连接
tlsState.sslCon = NULL;
}
#endif
}
在非内置 TLS 模式下刷新给定的 FILE 句柄
/* In non-builtin-TLS mode, fflush()es the given FILE handle, else this is a no-op.
用于在非内置 TLS 模式下刷新给定的 FILE 句柄,否则这个函数不执行任何操作。
*/
static void althttpd_fflush(FILE *f){
if( useHttps!=2 ){//不是内置 TLS 模式
fflush(f);//将缓冲区的数据写入文件
}
}
althttpd_exit 程序退出时
/* Flush the buffer then exit. */
static void althttpd_exit(void){
althttpd_fflush(stdout);
tls_close_conn();
exit(0);
}
BlockIPAddress,ServiceUnavailable
实现一个简单的 IP 阻止机制,用于防止来自特定 IP 地址的滥用请求。
/* Forward reference */
static void BlockIPAddress(void);
static void ServiceUnavailable(int lineno);
/*
** An abusive HTTP request has been submitted by the IP address zRemoteAddr.
** Block future requests coming from this IP address.
**
** This only happens if the zIPShunDir variable is set, which is only set
** by the --ipshun command-line option. Without that setting, this routine
** is a no-op.
**
** If zIPShunDir is a valid directory, then this routine uses zRemoteAddr
** as the name of a file within that directory. Cases:
**
** + The file already exists and is not an empty file. This will be the
** case if the same IP was recently blocked, but the block has expired,
** and yet the expiration was not so long ago that the blocking file has
** been unlinked. In this case, add one character to the file, which
** will update its mtime (causing it to be active again) and increase
** its expiration timeout.
**
** + The file exists and is empty. This happens if the administrator
** uses "touch" to create the file. An empty blocking file indicates
** a permanent block. Do nothing.
**
** + The file does not exist. Create it anew and make it one byte in size.
**
** The UnlinkExpiredIPBlockers() routine will run from time to time to
** unlink expired blocker files. If the DisallowedRemoteAddr() routine finds
** an expired blocker file corresponding to zRemoteAddr, it might unlink
** that one blocker file if the file has been expired for long enough.
这个函数是用来阻止恶意的 HTTP 请求来自 IP 地址 zRemoteAddr。它会将来自该 IP 地址的未来请求阻止。关键的步骤如下:
检查是否启用了 IP 阻止,即 zIPShunDir 不为空。
构建文件名 zFullname,该文件名是由 IP 阻止目录和远程地址构成的。
检查文件是否存在以及文件大小的情况。
根据文件的情况执行不同的操作,例如更新文件的 mtime、增加文件的大小等。
*/
static void BlockIPAddress(void){
size_t nIPShunDir;
size_t nRemoteAddr;
int rc;
struct stat statbuf;
char zFullname[1000];
if( zIPShunDir==0 ) return;
if( zRemoteAddr==0 ) return;
if( zRemoteAddr[0]==0 ) return;
/* If we reach this point, it means that a suspicious request was
** received and we want to activate IP blocking on the remote address.
** 如果到达此点,说明收到了一个可疑请求,我们希望在远程地址上激活 IP 阻止。
*/
nIPShunDir = strlen(zIPShunDir);
while( nIPShunDir>0 && zIPShunDir[nIPShunDir-1]=='/' ) nIPShunDir--;
nRemoteAddr = strlen(zRemoteAddr);
if( nIPShunDir + nRemoteAddr + 2 >= sizeof(zFullname) ){
Malfunction(914, /* LOG: buffer overflow */
"buffer overflow");
}
memcpy(zFullname, zIPShunDir, nIPShunDir);
zFullname[nIPShunDir] = '/';
memcpy(zFullname+nIPShunDir+1, zRemoteAddr, nRemoteAddr+1);
rc = stat(zFullname, &statbuf);
if( rc!=0 || statbuf.st_size>0 ){
FILE *lock = fopen(zFullname, "a");
if( lock ){
fputc('X', lock);
fclose(lock);
}
}
}
/*
** Send a service-unavailable reply.发送一个服务不可用的响应。
它会返回 HTTP 503 状态码和一条包含被滥用的 IP 地址信息的消息。
同时,它会关闭连接并记录日志。
*/
static void ServiceUnavailable(int lineno){
StartResponse("503 Service Unavailable");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"Service to IP address %s temporarily blocked due to abuse\n",
zRemoteAddr
);
closeConnection = 1;
MakeLogEntry(0, lineno);
althttpd_exit();
}
SetEnv
这个函数负责设置环境变量的值,并在检测到可能的攻击时采取相应的安全措施。
/* Set the value of environment variable zVar to zValue.
设置环境变量 zVar 的值为 zValue。*/
static void SetEnv(const char *zVar, const char *zValue){
char *z;
size_t len;
if( zValue==0 ) zValue="";
/* Disable an attempted bashdoor attack 禁用尝试的 bashdoor 攻击 */
if( strncmp(zValue,"() {",4)==0 ){//检查 zValue 是否以 "()" 开头,这是一种 bashdoor 攻击的尝试。
BlockIPAddress();
ServiceUnavailable(902); /* LOG: 902 bashdoor attack */
zValue = "";
}
len = strlen(zVar) + strlen(zValue) + 2;
z = SafeMalloc(len);
sprintf(z,"%s=%s",zVar,zValue);
putenv(z);
}
GetFirstElement
从输入字符串中提取第一个空格分隔的标记,并返回指向该标记的指针。
/*
** Remove the first space-delimited token from a string and return
** a pointer to it. Add a NULL to the string to terminate the token.
** Make *zLeftOver point to the start of the next token.
** 从字符串中移除第一个空格分隔的标记并返回指向它的指针。
** 在字符串末尾添加一个 NULL 以终止标记。
** 使 *zLeftOver 指向下一个标记的开始。
*/
static char *GetFirstElement(char *zInput, char **zLeftOver){
char *zResult = 0;
if( zInput==0 ){
if( zLeftOver ) *zLeftOver = 0;
return 0;
}
while( isspace(*(unsigned char*)zInput) ){ zInput++; }
zResult = zInput;
while( *zInput && !isspace(*(unsigned char*)zInput) ){ zInput++; }
if( *zInput ){
*zInput = 0;
zInput++;
while( isspace(*(unsigned char*)zInput) ){ zInput++; }
}
if( zLeftOver ){ *zLeftOver = zInput; }
return zResult;
}
StrDup,StrAppend
/* Make a copy of a string into memory obtained from malloc.
将字符串复制到使用 malloc 获得的内存中。*/
static char *StrDup(const char *zSrc){
char *zDest;
size_t size;
if( zSrc==0 ) return 0;
size = strlen(zSrc) + 1;
zDest = (char*)SafeMalloc( size );
strcpy(zDest,zSrc);
return zDest;
}
//将字符串附加到先前的字符串中,使用指定的分隔符。
static char *StrAppend(char *zPrior, const char *zSep, const char *zSrc){
char *zDest;
size_t size;
size_t n0, n1, n2;
if( zSrc==0 ) return 0;
if( zPrior==0 ) return StrDup(zSrc);
n0 = strlen(zPrior);
n1 = strlen(zSep);
n2 = strlen(zSrc);
size = n0+n1+n2+1;
zDest = (char*)SafeMalloc( size );
memcpy(zDest, zPrior, n0);
free(zPrior);
memcpy(&zDest[n0],zSep,n1);
memcpy(&zDest[n0+n1],zSrc,n2+1);
return zDest;
}
ComputeRequestUri,CompareEtags,RemoveNewline,Rfc822Date,DateTag
/*
** Construct the REQUEST_URI value from zString and zQueryString.
** 从 zString 和 zQueryString 构造 REQUEST_URI 值。
** REQUEST_URI is nominally the second field of the first line of the
** HTTP request. But we might have done some sanitization on the
** SCRIPT_NAME and/or PATH_INFO and we want to capture that in the
** REQUEST_URI. Hence, the REQUEST_URI is recomputed before being
** sent to CGI or SCGI.
** REQUEST_URI 名义上是 HTTP 请求的第一行的第二个字段。但我们可能对 SCRIPT_NAME 和/或 PATH_INFO 进行了一些净化,
** 我们希望在 REQUEST_URI 中捕获这些内容。因此,在发送到 CGI 或 SCGI 之前,会重新计算 REQUEST_URI。
*/
static void ComputeRequestUri(void){
if( zQueryString==0 || zQueryString[0]==0 ){
zRequestUri = zScript;
}else{
zRequestUri = StrAppend(zScript, "?", zQueryString);
}
}
/* Compare two ETag values. Return 0 if they match and non-zero if they differ.
** The one on the left might be a NULL pointer and it might be quoted.
比较两个 ETag 值。如果它们匹配则返回 0,否则返回非零。
左边的值可能是 NULL 指针,也可能带引号。
*/
static int CompareEtags(const char *zA, const char *zB){
if( zA==0 ) return 1;
if( zA[0]=='"' ){
int lenB = (int)strlen(zB);
if( strncmp(zA+1, zB, lenB)==0 && zA[lenB+1]=='"' ) return 0;
}
return strcmp(zA, zB);
}
/* Break a line at the first \n or \r character seen.
在第一个看到的 \n 或 \r 字符处中断一行。*/
static void RemoveNewline(char *z){
if( z==0 ) return;
while( *z && *z!='\n' && *z!='\r' ){ z++; }
*z = 0;
}
RFC822-formatted timestamp
/* Render seconds since 1970 as an RFC822 date string.
** Return a pointer to that string in a static buffer.
将自1970年以来的秒数呈现为 RFC822 日期字符串。
返回指向静态缓冲区中该字符串的指针。
*/
static char *Rfc822Date(time_t t){
struct tm *tm;
static char zDate[100];
tm = gmtime(&t);
strftime(zDate, sizeof(zDate), "%a, %d %b %Y %H:%M:%S GMT", tm);
return zDate;
}
/* Print a date tag in the header. The name of the tag is zTag.
** The date is determined from the unix timestamp given.
** 在头部打印日期标签。标签的名称是 zTag。
** 日期是从给定的 Unix 时间戳确定的。
*/
static int DateTag(const char *zTag, time_t t){
return althttpd_printf("%s: %s\r\n", zTag, Rfc822Date(t));
}
/*
** Parse an RFC822-formatted timestamp as we'd expect from HTTP and return
** a Unix epoch time. <= zero is returned on failure.
*/
time_t ParseRfc822Date(const char *zDate){
int mday, mon, year, yday, hour, min, sec;
char zIgnore[4];
char zMonth[4];
static const char *const azMonths[] =
{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
if( 7==sscanf(zDate, "%3[A-Za-z], %d %3[A-Za-z] %d %d:%d:%d", zIgnore,
&mday, zMonth, &year, &hour, &min, &sec)){
if( year > 1900 ) year -= 1900;
for(mon=0; mon<12; mon++){
if( !strncmp( azMonths[mon], zMonth, 3 )){
int nDay;
int isLeapYr;
static int priorDays[] =
{ 0, 31, 59, 90,120,151,181,212,243,273,304,334 };
isLeapYr = year%4==0 && (year%100!=0 || (year+300)%400==0);
yday = priorDays[mon] + mday - 1;
if( isLeapYr && mon>1 ) yday++;
nDay = (year-70)*365 + (year-69)/4 - year/100 + (year+300)/400 + yday;
return ((time_t)(nDay*24 + hour)*60 + min)*60 + sec;
}
}
}
return 0;
}
/* Test procedure for ParseRfc822Date */
void TestParseRfc822Date(void){
time_t t1, t2;
for(t1=0; t1<0x7fffffff; t1 += 127){
t2 = ParseRfc822Date(Rfc822Date(t1));
assert( t1==t2 );
}
}
StartResponse
/* Print the first line of a response followed by the server type.
打印响应的第一行,后面跟上服务器类型。*/
static void StartResponse(const char *zResultCode){
time_t now;
time(&now);
if( statusSent ) return;
nOut += althttpd_printf("%s %s\r\n",
zProtocol ? zProtocol : "HTTP/1.1",
zResultCode);
strncpy(zReplyStatus, zResultCode, 3);
zReplyStatus[3] = 0;
if( zReplyStatus[0]>='4' ){
closeConnection = 1;
}
if( closeConnection ){
nOut += althttpd_printf("Connection: close\r\n");
}else{
nOut += althttpd_printf("Connection: keep-alive\r\n");
}
nOut += DateTag("Date", now);
statusSent = 1;
}
UnlinkExpiredIPBlockers
/*
** Check all of the files in the zIPShunDir directory. Unlink any
** files in that directory that have expired.
** 检查 zIPShunDir 目录中的所有文件。解除任何在该目录中已过期的文件。
**
** This routine might be slow if there are a lot of blocker files.
** So it only runs when we are not in a hurry, such as prior to sending
** a 404 Not Found reply.
** 如果有很多阻止文件,这个例程可能会很慢。因此,它只在我们不急于执行时运行,
** 例如在发送 404 Not Found 回复之前。
*/
static void UnlinkExpiredIPBlockers(void){
DIR *pDir;
struct dirent *pFile;
size_t nIPShunDir;
time_t now;
char zFilename[2000];
if( zIPShunDir==0 ) return;
if( zIPShunDir[0]!='/' ) return;
nIPShunDir = strlen(zIPShunDir);
while( nIPShunDir>0 && zIPShunDir[nIPShunDir-1]=='/' ) nIPShunDir--;
if( nIPShunDir > sizeof(zFilename)-100 ) return;
memcpy(zFilename, zIPShunDir, nIPShunDir);
zFilename[nIPShunDir] = 0;
pDir = opendir(zFilename);
if( pDir==0 ) return;
zFilename[nIPShunDir] = '/';
time(&now);
while( (pFile = readdir(pDir))!=0 ){
size_t nFile = strlen(pFile->d_name);
int rc;
struct stat statbuf;
if( nIPShunDir+nFile >= sizeof(zFilename)-2 ) continue;
if( strstr(pFile->d_name, "..") ) continue;
memcpy(zFilename+nIPShunDir+1, pFile->d_name, nFile+1);
memset(&statbuf, 0, sizeof(statbuf));
rc = stat(zFilename, &statbuf);
if( rc ) continue;
if( !S_ISREG(statbuf.st_mode) ) continue;
if( statbuf.st_size==0 ) continue;
if( statbuf.st_size*5*BANISH_TIME + statbuf.st_mtime > now ) continue;
unlink(zFilename);// 解除文件
}
closedir(pDir);
}
LikelyHackAttempt 判断黑客
/* Return true if the request URI contained in zScript[] seems like a hack attempt.
如果包含在 zScript[] 中的请求 URI 看起来像是一次黑客尝试,则返回 true。
*/
static int LikelyHackAttempt(void){
static const char *azHackUris[] = {
"/../",
"/./",
"_SELECT_",
"_select_",
"_sleep_",
"_OR_",
"_AND_",
"/etc/passwd",
"/bin/sh",
"/.git/",
"/swagger.yaml",
"/phpThumb.php",
"/.htpasswd",
"/.passwd",
"/tomcat/manager/status/",
"/WEB-INF/jboss-web.xml",
"/phpMyAdmin/setup/index.php",
"/examples/feed-viewer/feed-proxy.php",
};
unsigned int i;
if( zScript==0 ) return 0;
if( zScript[0]==0 ) return 0;
if( zScript[0]!='/' ) return 1;
for(i=0; i<sizeof(azHackUris)/sizeof(azHackUris[0]); i++){
if( strstr(zScript, azHackUris[i])!=0 ) return 1;
}
return 0;
}
NotFound,Forbidden,NotAuthorized,CgiError
几种访问错误:404、403、401、500
/* Tell the client that there is no such document
告诉客户端找不到此文档*/
static void NotFound(int lineno){
if( LikelyHackAttempt() ){
BlockIPAddress();
ServiceUnavailable(lineno);
}
UnlinkExpiredIPBlockers();
StartResponse("404 Not Found");
nOut += althttpd_printf(
"Content-type: text/html; charset=utf-8\r\n"
"\r\n"
"<head><title lineno=\"%d\">Not Found</title></head>\n"
"<body><h1>Document Not Found</h1>\n"
"The document %s is not available on this server\n"
"</body>\n", lineno, zScript);
MakeLogEntry(0, lineno);
althttpd_exit();
}
/* Tell the client that they are not welcomed here.
告诉客户端在这里不受欢迎。*/
static void Forbidden(int lineno){
StartResponse("403 Forbidden");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"Access denied\n"
);
closeConnection = 1;
MakeLogEntry(0, lineno);
althttpd_exit();
}
/* Tell the client that authorization is required to access the document.
告诉客户端访问文档需要授权。*/
static void NotAuthorized(const char *zRealm){
StartResponse("401 Authorization Required");
nOut += althttpd_printf(
"WWW-Authenticate: Basic realm=\"%s\"\r\n"
"Content-type: text/html; charset=utf-8\r\n"
"\r\n"
"<head><title>Not Authorized</title></head>\n"
"<body><h1>401 Not Authorized</h1>\n"
"A login and password are required for this document\n"
"</body>\n", zRealm);
MakeLogEntry(0, 110); /* LOG: Not authorized */
}
/* Tell the client that there is an error in the script.
告诉客户端脚本中存在错误。*/
static void CgiError(void){
StartResponse("500 Error");
nOut += althttpd_printf(
"Content-type: text/html; charset=utf-8\r\n"
"\r\n"
"<head><title>CGI Program Error</title></head>\n"
"<body><h1>CGI Program Error</h1>\n"
"The CGI program %s generated an error\n"
"</body>\n", zScript);
MakeLogEntry(0, 120); /* LOG: CGI Error */
althttpd_exit();
exit(0);
}
Timeout,SetTimeout
用于处理超时设置和处理信号
/* Set the timeout in seconds. 0 means no-timeout.
设置超时时间,以秒为单位。0表示无超时。*/
static void SetTimeout(int nSec, int lineNum){
if( useTimeout ){
nTimeoutLine = lineNum;
alarm(nSec);//设置闹钟
}
}
/* This is called if we timeout or catch some other kind of signal.
** Log an error code which is 900+iSig and then quit.
** 如果发生超时或捕获其他类型的信号,则调用此函数。
** 记录错误代码,该错误代码为 900+iSig,然后退出。
*/
static void Timeout(int iSig){
if( !debugFlag ){
if( zScript && zScript[0] ){
char zBuf[10];
zBuf[0] = '9';
zBuf[1] = '0' + (iSig/10)%10;
zBuf[2] = '0' + iSig%10;
zBuf[3] = 0;
strcpy(zReplyStatus, zBuf);
++inSignalHandler;
switch( iSig ){
case SIGALRM:
MakeLogEntry(0, nTimeoutLine);
break;
case SIGSEGV:
MakeLogEntry(0, 131); /* LOG: SIGSEGV */
break;
case SIGPIPE:
MakeLogEntry(0, 132); /* LOG: SIGPIPE */
break;
case SIGXCPU:
MakeLogEntry(0, 133); /* LOG: SIGXCPU */
break;
default:
MakeLogEntry(0, 139); /* LOG: Unknown signal */
break;
}
--inSignalHandler;
}
exit(0);
}
}
Redirect,Decode64
服务器重定向
base-64 解码
/*
** Do a server redirect to the document specified. The document
** name not contain scheme or network location or the query string.
** It will be just the path.
执行服务器重定向到指定的文档。文档名不包含方案、网络位置或查询字符串。它只是路径。
*/
static void Redirect(const char *zPath, int iStatus, int finish, int lineno){
switch( iStatus ){
case 301:
StartResponse("301 Permanent Redirect");
break;
case 308:
StartResponse("308 Permanent Redirect");
break;
default:
StartResponse("302 Temporary Redirect");
break;
}
if( zServerPort==0 || zServerPort[0]==0 || strcmp(zServerPort,"80")==0 ){
nOut += althttpd_printf("Location: %s://%s%s%s\r\n",
zHttpScheme, zServerName, zPath, zQuerySuffix);
}else{
nOut += althttpd_printf("Location: %s://%s:%s%s%s\r\n",
zHttpScheme, zServerName, zServerPort, zPath, zQuerySuffix);
}
if( finish ){
nOut += althttpd_printf("Content-length: 0\r\n");
nOut += althttpd_printf("\r\n");
MakeLogEntry(0, lineno);
}
fflush(stdout);
}
/*
** This function treats its input as a base-64 string and returns the
** decoded value of that string. Characters of input that are not
** valid base-64 characters (such as spaces and newlines) are ignored.
此函数将其输入视为 base-64 字符串,并返回该字符串的解码值。
忽略不是有效 base-64 字符(如空格和换行符)的输入字符。
*/
static void Decode64(char *z64){
char *zData;
int n64;
int i, j;
int a, b, c, d;
static int isInit = 0;
static int trans[128];
static unsigned char zBase[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
if( !isInit ){
for(i=0; i<128; i++){ trans[i] = 0; }
for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
isInit = 1;
}
n64 = strlen(z64);
while( n64>0 && z64[n64-1]=='=' ) n64--;
zData = z64;
for(i=j=0; i+3<n64; i+=4){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
c = trans[z64[i+2] & 0x7f];
d = trans[z64[i+3] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
zData[j++] = ((c<<6) & 0xc0) | (d & 0x3f);
}
if( i+2<n64 ){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
c = trans[z64[i+2] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
}else if( i+1<n64 ){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
}
zData[j] = 0;
}
TLS:ssl_init_server,sslctx_use_pkey_from_mem,sslctx_use_cert_from_mem
初始化SSL服务器
加载SSL证书和私钥等操作。
#ifdef ENABLE_TLS
/* This is a self-signed cert in the PEM format that can be used when
** no other certs are available.
**
** NB: Use of this self-signed cert is wildly insecure. Use for testing
** purposes only.
*/
static const char sslSelfCert[] =
"-----BEGIN CERTIFICATE-----\n"
"MIIDMTCCAhkCFGrDmuJkkzWERP/ITBvzwwI2lv0TMA0GCSqGSIb3DQEBCwUAMFQx\n"
"CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMw\n"
"EQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYDVQQDDAZGb3NzaWwwIBcNMjExMjI3MTEz\n"
"MTU2WhgPMjEyMTEyMjcxMTMxNTZaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJO\n"
"QzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMwEQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYD\n"
"VQQDDAZGb3NzaWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCbTU2\n"
"6GRQHQqLq7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqX\n"
"xZlzmS/CglZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfe\n"
"fiIYPDk1GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlur\n"
"Tlv0rjsYOfq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12J\n"
"avhFcd4JU4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1k\n"
"KxJxXQh7rIYjm+RTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFkdtpqcybAzJN8G\n"
"+ONuUm5sXNbWta7JGvm8l0BTSBcCUtJA3hn16iJqXA9KmLnaF2denC4EYk+KlVU1\n"
"QXxskPJ4jB8A5B05jMijYv0nzCxKhviI8CR7GLEEGKzeg9pbW0+O3vaVehoZtdFX\n"
"z3SsCssr9QjCLiApQxMzW1Iv3od2JXeHBwfVMFrWA1VCEUCRs8OSW/VOqDPJLVEi\n"
"G6wxc4kN9dLK+5S29q3nzl24/qzXoF8P9Re5KBCbrwaHgy+OEEceq5jkmfGFxXjw\n"
"pvVCNry5uAhH5NqbXZampUWqiWtM4eTaIPo7Y2mDA1uWhuWtO6F9PsnFJlQHCnwy\n"
"s/TsrXk=\n"
"-----END CERTIFICATE-----\n";
/* This is the private-key corresponding to the cert above
*/
static const char sslSelfPKey[] =
"-----BEGIN PRIVATE KEY-----\n"
"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCCbTU26GRQHQqL\n"
"q7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqXxZlzmS/C\n"
"glZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfefiIYPDk1\n"
"GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlurTlv0rjsY\n"
"Ofq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12JavhFcd4J\n"
"U4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1kKxJxXQh7\n"
"rIYjm+RTAgMBAAECggEANfTH1vc8yIe7HRzmm9lsf8jF+II4s2705y2H5qY+cvYx\n"
"nKtZJGOG1X0KkYy7CGoFv5K0cSUl3lS5FVamM/yWIzoIex/Sz2C1EIL2aI5as6ez\n"
"jB6SN0/J+XI8+Vt7186/rHxfdIPpxuzjHbxX3HTpScETNWcLrghbrPxakbTPPxwt\n"
"+x7QlPmmkFNuMfvkzToFf9NdwL++44TeBPOpvD/Lrw+eyqdth9RJPq9cM96plh9V\n"
"HuRqeD8+QNafaXBdSQs3FJK/cDK/vWGKZWIfFVSDbDhwYljkXGijreFjtXQfkkpF\n"
"rl1J87/H9Ee7z8fTD2YXQHl+0/rghAVtac3u54dpQQKBgQC2XG3OEeMrOp9dNkUd\n"
"F8VffUg0ecwG+9L3LCe7U71K0kPmXjV6xNnuYcNQu84kptc5vI8wD23p29LaxdNc\n"
"9m0lcw06/YYBOPkNphcHkINYZTvVJF10mL3isymzMaTtwDkZUkOjL1B+MTiFT/qp\n"
"ARKrTYGJ4HxY7+tUkI5pUmg4PQKBgQC3GA4d1Rz3Pb/RRpcsZgWknKsKhoN36mSn\n"
"xFJ3wPBvVv2B1ltTMzh/+the0ty6clzMrvoLERzRcheDsNrc/j/TUVG8sVdBYJwX\n"
"tMZyFW4NVMOErT/1ukh6jBqIMBo6NJL3EV/AKj0yniksgKOr0/AAduAccnGST8Jd\n"
"SHOdjwvHzwKBgGZBq/zqgNTDuYseHGE07CMgcDWkumiMGv8ozlq3mSR0hUiPOTPP\n"
"YFjQjyIdPXnF6FfiyPPtIvgIoNK2LVAqiod+XUPf152l4dnqcW13dn9BvOxGyPTR\n"
"lWCikFaAFviOWjY9r9m4dU1dslDmySqthFd0TZgPvgps9ivkJ0cdw30NAoGAMC/E\n"
"h1VvKiK2OP27C5ROJ+STn1GHiCfIFd81VQ8SODtMvL8NifgRBp2eFFaqgOdYRQZI\n"
"CGGYlAbS6XXCJCdF5Peh62dA75PdgN+y2pOJQzjrvB9cle9Q4++7i9wdCvSLOTr5\n"
"WDnFoWy+qVexu6crovOmR9ZWzYrwPFy1EOJ010ECgYBl7Q+jmjOSqsVwhFZ0U7LG\n"
"diN+vXhWfn1wfOWd8u79oaqU/Oy7xyKW2p3H5z2KFrBM/vib53Lh4EwFZjcX+jVG\n"
"krAmbL+M/hP7z3TD2UbESAzR/c6l7FU45xN84Lsz5npkR8H/uAHuqLgb9e430Mjx\n"
"YNMwdb8rChHHChNZu6zuxw==\n"
"-----END PRIVATE KEY-----\n";
/*
** Read a PEM certificate from memory and push it into an SSL_CTX.
** Return the number of errors.
*/
static int sslctx_use_cert_from_mem(
SSL_CTX *ctx,
const char *pData,
int nData
){
BIO *in;
int rc = 1;
X509 *x = 0;
X509 *cert = 0;
in = BIO_new_mem_buf(pData, nData);
if( in==0 ) goto end_of_ucfm;
x = X509_new();
if( x==0 ) goto end_of_ucfm;
cert = PEM_read_bio_X509(in, &x, 0, 0);
if( cert==0 ) goto end_of_ucfm;
rc = SSL_CTX_use_certificate(ctx, x)<=0;
end_of_ucfm:
X509_free(x);
BIO_free(in);
return rc;
}
/*
** Read a PEM private key from memory and add it to an SSL_CTX.
** Return the number of errors.
*/
static int sslctx_use_pkey_from_mem(
SSL_CTX *ctx,
const char *pData,
int nData
){
int rc = 1;
BIO *in;
EVP_PKEY *pkey = 0;
in = BIO_new_mem_buf(pData, nData);
if( in==0 ) goto end_of_upkfm;
pkey = PEM_read_bio_PrivateKey(in, 0, 0, 0);
if( pkey==0 ) goto end_of_upkfm;
rc = SSL_CTX_use_PrivateKey(ctx, pkey)<=0;
EVP_PKEY_free(pkey);
end_of_upkfm:
BIO_free(in);
return rc;
}
/*
** Initialize the SSL library so that it is able to handle
** server-side connections. Invokes Malfunction() if there are
** any problems (so does not return on error).
**
** If zKeyFile and zCertFile are not NULL, then they are the names
** of disk files that hold the certificate and private-key for the
** server. If zCertFile is not NULL but zKeyFile is NULL, then
** zCertFile is assumed to be a concatenation of the certificate and
** the private-key in the PEM format.
**
** If zCertFile is "unsafe-builtin" then a built-in self-signed cert
** is used and zKeyFile is ignored.
**
** Error messages may contain the paths to the given files, but this
** function is called before the server starts listening for requests,
** so those will never be sent to clients.
*/
static void ssl_init_server(const char *zCertFile,
const char *zKeyFile){
if( tlsState.isInit==0 ){
const int useSelfSigned = zCertFile
&& 0==strcmp("unsafe-builtin", zCertFile);
SSL_library_init();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
tlsState.ctx = SSL_CTX_new(SSLv23_server_method());
if( tlsState.ctx==0 ){
ERR_print_errors_fp(stderr);
Malfunction(501, /* LOG: Error initializing the SSL Server */
"Error initializing the SSL server");
}
if( !useSelfSigned && zCertFile && zCertFile[0] ){
if( SSL_CTX_use_certificate_chain_file(tlsState.ctx,
zCertFile)!=1 ){
ERR_print_errors_fp(stderr);
Malfunction(502, /* LOG: Error loading CERT file */
"Error loading CERT file \"%s\"", zCertFile);
}
if( zKeyFile==0 ) zKeyFile = zCertFile;
if( SSL_CTX_use_PrivateKey_file(tlsState.ctx, zKeyFile,
SSL_FILETYPE_PEM)<=0 ){
ERR_print_errors_fp(stderr);
Malfunction(503, /* LOG: Error loading private key file */
"Error loading PRIVATE KEY from file \"%s\"",
zKeyFile);
}
}else if( useSelfSigned ){
if(sslctx_use_cert_from_mem(tlsState.ctx, sslSelfCert, -1)
|| sslctx_use_pkey_from_mem(tlsState.ctx, sslSelfPKey, -1) ){
Malfunction(504, /* LOG: Error loading self-signed cert */
"Error loading self-signed CERT");
}
}else{
Malfunction(505,"No certificate TLS specified"); /* LOG: No cert */
}
if( !SSL_CTX_check_private_key(tlsState.ctx) ){
Malfunction(506, /* LOG: private key does not match cert */
"PRIVATE KEY \"%s\" does not match CERT \"%s\"",
zKeyFile, zCertFile);
}
SSL_CTX_set_mode(tlsState.ctx, SSL_MODE_AUTO_RETRY);
tlsState.isInit = 2;
}else{
assert( tlsState.isInit==2 );
}
}
#endif /*ENABLE_TLS*/
CheckBasicAuthorization
验证用户是否具有访问权限。
从给定的文件(zAuthFile)中读取授权信息。支持的授权信息包括用户名和密码(“user NAME LOGIN:PASSWORD”),以及其他指令,如指定域(“realm TEXT”)、要求重定向至HTTPS(“http-redirect”)、仅允许HTTPS操作(“https-only”)等。
/*
** Check to see if basic authorization credentials are provided for
** the user according to the information in zAuthFile. Return true
** if authorized. Return false if not authorized.
**
** File format:
**
** * Blank lines and lines that begin with '#' are ignored
** * "http-redirect" forces a redirect to HTTPS if not there already
** * "https-only" disallows operation in HTTP
** * "user NAME LOGIN:PASSWORD" checks to see if LOGIN:PASSWORD
** authorization credentials are provided, and if so sets the
** REMOTE_USER to NAME.
** * "realm TEXT" sets the realm to TEXT.
** * "anyone" bypasses authentication and allows anyone to see the
** files. Useful in combination with "http-redirect"
*/
static int CheckBasicAuthorization(const char *zAuthFile){
FILE *in;
char *zRealm = "unknown realm";
char *zLoginPswd;
char *zName;
char zLine[2000];
in = fopen(zAuthFile, "rb");
if( in==0 ){
NotFound(150); /* LOG: Cannot open -auth file */
return 0;
}
if( zAuthArg ) Decode64(zAuthArg);
while( fgets(zLine, sizeof(zLine), in) ){
char *zFieldName;
char *zVal;
zFieldName = GetFirstElement(zLine,&zVal);
if( zFieldName==0 || *zFieldName==0 ) continue;
if( zFieldName[0]=='#' ) continue;
RemoveNewline(zVal);
if( strcmp(zFieldName, "realm")==0 ){
zRealm = StrDup(zVal);
}else if( strcmp(zFieldName,"user")==0 ){
if( zAuthArg==0 ) continue;
zName = GetFirstElement(zVal, &zVal);
zLoginPswd = GetFirstElement(zVal, &zVal);
if( zLoginPswd==0 ) continue;
if( zAuthArg && strcmp(zAuthArg,zLoginPswd)==0 ){
zRemoteUser = StrDup(zName);
fclose(in);
return 1;
}
}else if( strcmp(zFieldName,"https-only")==0 ){
if( !useHttps ){
NotFound(160); /* LOG: http request on https-only page */
fclose(in);
return 0;
}
}else if( strcmp(zFieldName,"http-redirect")==0 ){
if( !useHttps ){
zHttpScheme = "https";
Redirect(zScript, 301, 1, 170); /* LOG: -auth redirect */
fclose(in);
return 0;
}
}else if( strcmp(zFieldName,"anyone")==0 ){
fclose(in);
return 1;
}else{
NotFound(180); /* LOG: malformed entry in -auth file */
fclose(in);
return 0;
}
}
fclose(in);
NotAuthorized(zRealm);
return 0;
}
mimetypes
根据文件名猜测文档的MIME类型
通过对文件名后缀进行二分查找,从硬编码的MIME类型映射表中获取与文件后缀对应的MIME类型。每个MIME类型由文件后缀、后缀长度、标志位以及MIME类型本身组成。
MIME类型(Multipurpose Internet Mail Extensions)是一种在互联网上标识文件类型的标准。它最初是为电子邮件附件设计的,但后来被广泛用于HTTP协议中,以指示在Web上传输的文件的类型和性质。
/*
** Type for mapping file extensions to mimetypes and type-specific
** internal flags.
*/
typedef struct MimeTypeDef {
const char *zSuffix; /* The file suffix */
unsigned char size; /* Length of the suffix */
unsigned char flags; /* See the MTF_xxx flags macros */
const char *zMimetype; /* The corresponding mimetype */
} MimeTypeDef;
/* Flags for mimetype flags. These MUST match the values hard-coded in
** GetMimeType(). That function avoids the macros for space reasons. */
#define MTF_NOCGI 0x1 /* Never treat as CGI */
#define MTF_NOCHARSET 0x2 /* Elide charset=... from Content-Type */
/*
** Guess the mime-type of a document based on its name.
*/
const MimeTypeDef *GetMimeType(const char *zName, int nName){
const char *z;
int i;
int first, last;
int len;
char zSuffix[20];
/* A table of mimetypes based on file suffixes.
** Suffixes must be in sorted order so that we can do a binary
** search to find the mime-type
*/
static const MimeTypeDef aMime[] = {
{ "ai", 2, 0x00, "application/postscript" },
{ "aif", 3, 0x00, "audio/x-aiff" },
{ "aifc", 4, 0x00, "audio/x-aiff" },
{ "aiff", 4, 0x00, "audio/x-aiff" },
{ "arj", 3, 0x00, "application/x-arj-compressed" },
{ "asc", 3, 0x00, "text/plain" },
{ "asf", 3, 0x00, "video/x-ms-asf" },
{ "asx", 3, 0x00, "video/x-ms-asx" },
{ "au", 2, 0x00, "audio/ulaw" },
{ "avi", 3, 0x00, "video/x-msvideo" },
{ "bat", 3, 0x00, "application/x-msdos-program" },
{ "bcpio", 5, 0x00, "application/x-bcpio" },
{ "bin", 3, 0x00, "application/octet-stream" },
{ "c", 1, 0x00, "text/plain" },
{ "cc", 2, 0x00, "text/plain" },
{ "ccad", 4, 0x00, "application/clariscad" },
{ "cdf", 3, 0x00, "application/x-netcdf" },
{ "class", 5, 0x00, "application/octet-stream" },
{ "cod", 3, 0x00, "application/vnd.rim.cod" },
{ "com", 3, 0x00, "application/x-msdos-program" },
{ "cpio", 4, 0x00, "application/x-cpio" },
{ "cpt", 3, 0x00, "application/mac-compactpro" },
{ "csh", 3, 0x00, "application/x-csh" },
{ "css", 3, 0x00, "text/css" },
{ "dcr", 3, 0x00, "application/x-director" },
{ "deb", 3, 0x00, "application/x-debian-package" },
{ "dir", 3, 0x00, "application/x-director" },
{ "dl", 2, 0x00, "video/dl" },
{ "dms", 3, 0x00, "application/octet-stream" },
{ "doc", 3, 0x00, "application/msword" },
{ "drw", 3, 0x00, "application/drafting" },
{ "dvi", 3, 0x00, "application/x-dvi" },
{ "dwg", 3, 0x00, "application/acad" },
{ "dxf", 3, 0x00, "application/dxf" },
{ "dxr", 3, 0x00, "application/x-director" },
{ "eps", 3, 0x00, "application/postscript" },
{ "etx", 3, 0x00, "text/x-setext" },
{ "exe", 3, 0x00, "application/octet-stream" },
{ "ez", 2, 0x00, "application/andrew-inset" },
{ "f", 1, 0x00, "text/plain" },
{ "f90", 3, 0x00, "text/plain" },
{ "fli", 3, 0x00, "video/fli" },
{ "flv", 3, 0x00, "video/flv" },
{ "gif", 3, 0x02, "image/gif" },
{ "gl", 2, 0x00, "video/gl" },
{ "gtar", 4, 0x00, "application/x-gtar" },
{ "gz", 2, 0x00, "application/x-gzip" },
{ "hdf", 3, 0x00, "application/x-hdf" },
{ "hh", 2, 0x00, "text/plain" },
{ "hqx", 3, 0x00, "application/mac-binhex40" },
{ "h", 1, 0x00, "text/plain" },
{ "htm", 3, 0x00, "text/html" },
{ "html", 4, 0x00, "text/html" },
{ "ice", 3, 0x00, "x-conference/x-cooltalk" },
{ "ief", 3, 0x00, "image/ief" },
{ "iges", 4, 0x00, "model/iges" },
{ "igs", 3, 0x00, "model/iges" },
{ "ips", 3, 0x00, "application/x-ipscript" },
{ "ipx", 3, 0x00, "application/x-ipix" },
{ "jad", 3, 0x00, "text/vnd.sun.j2me.app-descriptor" },
{ "jar", 3, 0x00, "application/java-archive" },
{ "jpeg", 4, 0x02, "image/jpeg" },
{ "jpe", 3, 0x00, "image/jpeg" },
{ "jpg", 3, 0x02, "image/jpeg" },
{ "js", 2, 0x00, "text/x-javascript" },
/* application/javascript is commonly used for JS, but the
** HTML spec says text/javascript is correct:
** https://html.spec.whatwg.org/multipage/scripting.html
** #scriptingLanguages:javascript-mime-type */
{ "json", 4, 0x00, "application/json" },
{ "kar", 3, 0x00, "audio/midi" },
{ "latex", 5, 0x00, "application/x-latex" },
{ "lha", 3, 0x00, "application/octet-stream" },
{ "lsp", 3, 0x00, "application/x-lisp" },
{ "lzh", 3, 0x02, "application/octet-stream" },
{ "m", 1, 0x00, "text/plain" },
{ "m3u", 3, 0x00, "audio/x-mpegurl" },
{ "man", 3, 0x00, "application/x-troff-man" },
{ "me", 2, 0x00, "application/x-troff-me" },
{ "mesh", 4, 0x00, "model/mesh" },
{ "mid", 3, 0x00, "audio/midi" },
{ "midi", 4, 0x02, "audio/midi" },
{ "mif", 3, 0x00, "application/x-mif" },
{ "mime", 4, 0x00, "www/mime" },
{ "mjs", 3, 0x00, "text/javascript" /*ES6 module*/ },
{ "movie", 5, 0x00, "video/x-sgi-movie" },
{ "mov", 3, 0x02, "video/quicktime" },
{ "mp2", 3, 0x02, "audio/mpeg" },
{ "mp3", 3, 0x02, "audio/mpeg" },
{ "mpeg", 4, 0x02, "video/mpeg" },
{ "mpe", 3, 0x00, "video/mpeg" },
{ "mpga", 4, 0x00, "audio/mpeg" },
{ "mpg", 3, 0x02, "video/mpeg" },
{ "ms", 2, 0x00, "application/x-troff-ms" },
{ "msh", 3, 0x00, "model/mesh" },
{ "nc", 2, 0x00, "application/x-netcdf" },
{ "oda", 3, 0x00, "application/oda" },
{ "ogg", 3, 0x00, "application/ogg" },
{ "ogm", 3, 0x00, "application/ogg" },
{ "pbm", 3, 0x00, "image/x-portable-bitmap" },
{ "pdb", 3, 0x00, "chemical/x-pdb" },
{ "pdf", 3, 0x00, "application/pdf" },
{ "pgm", 3, 0x00, "image/x-portable-graymap" },
{ "pgn", 3, 0x00, "application/x-chess-pgn" },
{ "pgp", 3, 0x00, "application/pgp" },
{ "pl", 2, 0x00, "application/x-perl" },
{ "pm", 2, 0x00, "application/x-perl" },
{ "png", 3, 0x02, "image/png" },
{ "pnm", 3, 0x00, "image/x-portable-anymap" },
{ "pot", 3, 0x00, "application/mspowerpoint" },
{ "ppm", 3, 0x00, "image/x-portable-pixmap" },
{ "pps", 3, 0x00, "application/mspowerpoint" },
{ "ppt", 3, 0x00, "application/mspowerpoint" },
{ "ppz", 3, 0x00, "application/mspowerpoint" },
{ "pre", 3, 0x00, "application/x-freelance" },
{ "prt", 3, 0x00, "application/pro_eng" },
{ "ps", 2, 0x00, "application/postscript" },
{ "qt", 2, 0x00, "video/quicktime" },
{ "ra", 2, 0x00, "audio/x-realaudio" },
{ "ram", 3, 0x00, "audio/x-pn-realaudio" },
{ "rar", 3, 0x00, "application/x-rar-compressed" },
{ "ras", 3, 0x00, "image/cmu-raster" },
{ "rgb", 3, 0x00, "image/x-rgb" },
{ "rm", 2, 0x00, "audio/x-pn-realaudio" },
{ "roff", 4, 0x00, "application/x-troff" },
{ "rpm", 3, 0x00, "audio/x-pn-realaudio-plugin" },
{ "rtf", 3, 0x00, "text/rtf" },
{ "rtx", 3, 0x00, "text/richtext" },
{ "scm", 3, 0x00, "application/x-lotusscreencam" },
{ "set", 3, 0x00, "application/set" },
{ "sgml", 4, 0x00, "text/sgml" },
{ "sgm", 3, 0x00, "text/sgml" },
{ "sh", 2, 0x00, "application/x-sh" },
{ "shar", 4, 0x00, "application/x-shar" },
{ "silo", 4, 0x00, "model/mesh" },
{ "sit", 3, 0x00, "application/x-stuffit" },
{ "skd", 3, 0x00, "application/x-koan" },
{ "skm", 3, 0x00, "application/x-koan" },
{ "skp", 3, 0x00, "application/x-koan" },
{ "skt", 3, 0x00, "application/x-koan" },
{ "smi", 3, 0x00, "application/smil" },
{ "smil", 4, 0x00, "application/smil" },
{ "snd", 3, 0x00, "audio/basic" },
{ "sol", 3, 0x00, "application/solids" },
{ "spl", 3, 0x00, "application/x-futuresplash" },
{ "src", 3, 0x00, "application/x-wais-source" },
{ "step", 4, 0x00, "application/STEP" },
{ "stl", 3, 0x00, "application/SLA" },
{ "stp", 3, 0x00, "application/STEP" },
{ "sv4cpio", 7, 0x00, "application/x-sv4cpio" },
{ "sv4crc", 6, 0x00, "application/x-sv4crc" },
{ "svg", 3, 0x00, "image/svg+xml" },
{ "swf", 3, 0x00, "application/x-shockwave-flash" },
{ "t", 1, 0x00, "application/x-troff" },
{ "tar", 3, 0x00, "application/x-tar" },
{ "tcl", 3, 0x00, "application/x-tcl" },
{ "tex", 3, 0x00, "application/x-tex" },
{ "texi", 4, 0x00, "application/x-texinfo" },
{ "texinfo", 7, 0x00, "application/x-texinfo" },
{ "tgz", 3, 0x00, "application/x-tar-gz" },
{ "tiff", 4, 0x00, "image/tiff" },
{ "tif", 3, 0x00, "image/tiff" },
{ "tr", 2, 0x00, "application/x-troff" },
{ "tsi", 3, 0x00, "audio/TSP-audio" },
{ "tsp", 3, 0x00, "application/dsptype" },
{ "tsv", 3, 0x00, "text/tab-separated-values" },
{ "txt", 3, 0x00, "text/plain" },
{ "unv", 3, 0x00, "application/i-deas" },
{ "ustar", 5, 0x00, "application/x-ustar" },
{ "vcd", 3, 0x00, "application/x-cdlink" },
{ "vda", 3, 0x00, "application/vda" },
{ "viv", 3, 0x00, "video/vnd.vivo" },
{ "vivo", 4, 0x00, "video/vnd.vivo" },
{ "vrml", 4, 0x00, "model/vrml" },
{ "vsix", 4, 0x00, "application/vsix" },
{ "wasm", 4, 0x03, "application/wasm" },
{ "wav", 3, 0x00, "audio/x-wav" },
{ "wax", 3, 0x00, "audio/x-ms-wax" },
{ "wiki", 4, 0x00, "application/x-fossil-wiki" },
{ "wma", 3, 0x00, "audio/x-ms-wma" },
{ "wmv", 3, 0x00, "video/x-ms-wmv" },
{ "wmx", 3, 0x00, "video/x-ms-wmx" },
{ "wrl", 3, 0x00, "model/vrml" },
{ "wvx", 3, 0x00, "video/x-ms-wvx" },
{ "xbm", 3, 0x00, "image/x-xbitmap" },
{ "xlc", 3, 0x00, "application/vnd.ms-excel" },
{ "xll", 3, 0x00, "application/vnd.ms-excel" },
{ "xlm", 3, 0x00, "application/vnd.ms-excel" },
{ "xls", 3, 0x00, "application/vnd.ms-excel" },
{ "xlw", 3, 0x00, "application/vnd.ms-excel" },
{ "xml", 3, 0x00, "text/xml" },
{ "xpm", 3, 0x00, "image/x-xpixmap" },
{ "xwd", 3, 0x00, "image/x-xwindowdump" },
{ "xyz", 3, 0x00, "chemical/x-pdb" },
{ "zip", 3, 0x00, "application/zip" },
};
for(i=nName-1; i>0 && zName[i]!='.'; i--){}
z = &zName[i+1];
len = nName - i;
if( len<(int)sizeof(zSuffix)-1 ){
strcpy(zSuffix, z);
for(i=0; zSuffix[i]; i++) zSuffix[i] = tolower(zSuffix[i]);
first = 0;
last = sizeof(aMime)/sizeof(aMime[0]);
while( first<=last ){
int c;
i = (first+last)/2;
c = strcmp(zSuffix, aMime[i].zSuffix);
if( c==0 ) return &aMime[i];
if( c<0 ){
last = i-1;
}else{
first = i+1;
}
}
}
return 0;
}
sanitizeString
URL处理和清理
/*
** The following table contains 1 for all characters that are permitted in
** the part of the URL before the query parameters and fragment.
**
** Allowed characters: 0-9a-zA-Z,-./:_~
**
** Disallowed characters include: !"#$%&'()*+;<=>?@[\]^`{|}
*/
static const char allowedInName[] = {
/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */
/* 0x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 1x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 2x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
/* 3x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
/* 4x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
/* 5x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
/* 6x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
/* 7x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
/* 8x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 9x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Ax */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Bx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Cx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Dx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Ex */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Fx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
/*
** Remove all disallowed characters in the input string z[]. Convert any
** disallowed characters into "_".
**
** Not that the three character sequence "%XX" where X is any byte is
** converted into a single "_" character.
**
** Return the number of characters converted. An "%XX" -> "_" conversion
** counts as a single character.
*/
static int sanitizeString(char *z){//替换非法字符
int nChange = 0;
while( *z ){
if( !allowedInName[*(unsigned char*)z] ){
char cNew = '_';
if( *z=='%' && z[1]!=0 && z[2]!=0 ){
int i;
if( z[1]=='2' ){
if( z[2]=='e' || z[2]=='E' ) cNew = '.';
if( z[2]=='f' || z[2]=='F' ) cNew = '/';
}
for(i=3; (z[i-2] = z[i])!=0; i++){}
}
*z = cNew;
nChange++;
}
z++;
}
return nChange;
}
/*
** Count the number of "/" characters in a string.
*/
static int countSlashes(const char *z){
int n = 0;
while( *z ) if( *(z++)=='/' ) n++;
return n;
}
tls_new_server,tls_close_server,tls_atexit
创建tls服务端
#ifdef ENABLE_TLS
/*
** Create a new server-side codec. The argument is the socket's file
** descriptor from which the codec reads and writes. The returned
** memory must eventually be passed to tls_close_server().
*/
static void *tls_new_server(int iSocket){
TlsServerConn *pServer = malloc(sizeof(*pServer));
BIO *b = pServer ? BIO_new_socket(iSocket, 0) : NULL;
if( NULL==b ){
Malfunction(507,"Cannot allocate TlsServerConn."); /* LOG: TlsServerConn */
}
assert(NULL!=tlsState.ctx);
pServer->ssl = SSL_new(tlsState.ctx);
pServer->bio = b;
pServer->iSocket = iSocket;
SSL_set_bio(pServer->ssl, b, b);
SSL_accept(pServer->ssl);
return (void*)pServer;
}
/*
** Close a server-side code previously returned from tls_new_server().
*/
static void tls_close_server(void *pServerArg){
TlsServerConn *pServer = (TlsServerConn*)pServerArg;
SSL_free(pServer->ssl);
memset(pServer, 0, sizeof(TlsServerConn));
free(pServer);
}
static void tls_atexit(void){
#if 0
/*
** Shutting down TLS can lead to spurious hung processes on some
** platforms/builds. See the long discussion on this at:
** https://sqlite.org/althttpd/forumpost/4dc31619341ce947
*/
if( inSignalHandler==0 && tlsState.sslCon!=0 ){
tls_close_server(tlsState.sslCon);
tlsState.sslCon = NULL;
}
#endif
}
#endif /* ENABLE_TLS */
althttpd_fgets,althttpd_fread,althttpd_fwrite,althttpd_fflush
封装fgets、tls_gets为althttpd_fgets
fread、fwrite、fflush同理
/*
** Works like fgets():
**
** Read a single line of input into s[]. Ensure that s[] is zero-terminated.
** The s[] buffer is size bytes and so at most size-1 bytes will be read.
**
** Return a pointer to s[] on success, or NULL at end-of-input.
**
** If in TLS mode, the final argument is ignored and the TLS
** connection is read instead.
*/
static char *althttpd_fgets(char *s, int size, FILE *in){
if( useHttps!=2 ){
return fgets(s, size, in);
}
#ifdef ENABLE_TLS
assert(NULL!=tlsState.sslCon);
return tls_gets(tlsState.sslCon, s, size);
#else
Malfunction(508,"SSL not available"); /* LOG: SSL not available */
return NULL;
#endif
}
/*
** Works like fread() but may, depending on connection state, use
** libssl to read the data (in which case the final argument is
** ignored). The target buffer must be at least (sz*nmemb) bytes.
*/
static size_t althttpd_fread(void *tgt, size_t sz, size_t nmemb, FILE *in){
if( useHttps!=2 ){
return fread(tgt, sz, nmemb, in);
}
#ifdef ENABLE_TLS
assert(NULL!=tlsState.sslCon);
return tls_read_server(tlsState.sslCon, tgt, sz*nmemb);
#else
Malfunction(509,"SSL not available"); /* LOG: SSL not available */
return 0;
#endif
}
/*
** Works like fwrite() but may, depending on connection state, write to
** the active TLS connection (in which case the final argument is
** ignored).
**
*/
static size_t althttpd_fwrite(
void const *src, /* Buffer containing content to write */
size_t sz, /* Size of each element in the buffer */
size_t nmemb, /* Number of elements to write */
FILE *out /* Write on this stream */
){
if( useHttps!=2 ){
return fwrite(src, sz, nmemb, out);
}
#ifdef ENABLE_TLS
assert(NULL!=tlsState.sslCon);
return tls_write_server(tlsState.sslCon, src, sz*nmemb);
#else
Malfunction(510,"SSL not available"); /* LOG: SSL not available */
return 0;
#endif
}
/*
** In non-builtin-TLS mode, fflush()es the given FILE handle, else
** this is a no-op.
*/
static void althttpd_fflush(FILE *f){
if( useHttps!=2 ){
fflush(f);
}
}
xferBytes
/*
** Transfer nXfer bytes from in to out, after first discarding
** nSkip bytes from in. Increment the nOut global variable
** according to the number of bytes transferred.
** 从输入流in中跳过nSkip字节后,将nXfer字节从in传输到out。根据传输的字节数,增加全局变量nOut。
**
** When running in built-in TLS mode the 2nd argument is ignored and
** output is instead sent via the TLS connection.
** 当以内建TLS模式运行时,第二个参数会被忽略,输出将通过TLS连接发送。
*/
static void xferBytes(FILE *in, FILE *out, int nXfer, int nSkip){
size_t n;
size_t got;
char zBuf[16384];
while( nSkip>0 ){
n = nSkip;
if( n>sizeof(zBuf) ) n = sizeof(zBuf);
got = fread(zBuf, 1, n, in);
if( got==0 ) break;
nSkip -= got;
}
while( nXfer>0 ){
n = nXfer;
if( n>sizeof(zBuf) ) n = sizeof(zBuf);
got = fread(zBuf, 1, n, in);
if( got==0 ) break;
althttpd_fwrite(zBuf, got, 1, out);
nOut += got;
nXfer -= got;
}
}
SendFile
/*
** Send the text of the file named by zFile as the reply. Use the
** suffix on the end of the zFile name to determine the mimetype.
** 将名为zFile的文件的文本内容作为回复发送。使用zFile名称末尾的后缀确定MIME类型。
**
** Return 1 to omit making a log entry for the reply.
** 返回1以避免为回复生成日志条目。
*/
static int SendFile(
const char *zFile, /* Name of the file to send */
int lenFile, /* Length of the zFile name in bytes */
struct stat *pStat /* Result of a stat() against zFile */
){
const char *zContentType;
time_t t;
FILE *in;
size_t szFilename;
char zETag[100];
const MimeTypeDef *pMimeType;//根据文件名的后缀确定MIME类型
int bAddCharset = 1;
const char *zEncoding = 0;
struct stat statbuf;
char zGzFilename[2000];
pMimeType = GetMimeType(zFile, lenFile);
zContentType = pMimeType
? pMimeType->zMimetype : "application/octet-stream";
if( pMimeType && (MTF_NOCHARSET & pMimeType->flags) ){
bAddCharset = 0;
}
if( zPostData ){ free(zPostData); zPostData = 0; }
sprintf(zETag, "m%xs%x", (int)pStat->st_mtime, (int)pStat->st_size);
if( CompareEtags(zIfNoneMatch,zETag)==0
|| (zIfModifiedSince!=0
&& (t = ParseRfc822Date(zIfModifiedSince))>0
&& t>=pStat->st_mtime)
){
StartResponse("304 Not Modified");
nOut += DateTag("Last-Modified", pStat->st_mtime);
nOut += althttpd_printf("Cache-Control: max-age=%d\r\n", mxAge);
nOut += althttpd_printf("ETag: \"%s\"\r\n", zETag);
nOut += althttpd_printf("\r\n");
fflush(stdout);
MakeLogEntry(0, 470); /* LOG: ETag Cache Hit */
return 1;
}
if( rangeEnd<=0
&& zAcceptEncoding
&& strstr(zAcceptEncoding,"gzip")!=0
){
szFilename = strlen(zFile);
if( szFilename < sizeof(zGzFilename)-10 ){
memcpy(zGzFilename, zFile, szFilename);
memcpy(zGzFilename + szFilename, ".gz", 4);
if( access(zGzFilename, R_OK)==0 ){
memset(&statbuf, 0, sizeof(statbuf));
if( stat(zGzFilename, &statbuf)==0 ){
zEncoding = "gzip";
zFile = zGzFilename;
pStat = &statbuf;
}
}
}
}
in = fopen(zFile,"rb");
if( in==0 ) NotFound(480); /* LOG: fopen() failed for static content */
if( rangeEnd>0 && rangeStart<pStat->st_size ){
StartResponse("206 Partial Content");
if( rangeEnd>=pStat->st_size ){
rangeEnd = pStat->st_size-1;
}
nOut += althttpd_printf("Content-Range: bytes %d-%d/%d\r\n",
rangeStart, rangeEnd, (int)pStat->st_size);
pStat->st_size = rangeEnd + 1 - rangeStart;
}else{
StartResponse("200 OK");
rangeStart = 0;
}
nOut += DateTag("Last-Modified", pStat->st_mtime);
if( enableSAB ){
/* The following two HTTP reply headers are required if javascript is to make use of SharedArrayBuffer
如果要使用SharedArrayBuffer,则以下两个HTTP回复标头是必需的 */
nOut += althttpd_printf("Cross-Origin-Opener-Policy: same-origin\r\n");
nOut += althttpd_printf("Cross-Origin-Embedder-Policy: require-corp\r\n");
}
nOut += althttpd_printf("Cache-Control: max-age=%d\r\n", mxAge);
nOut += althttpd_printf("ETag: \"%s\"\r\n", zETag);
nOut += althttpd_printf("Content-type: %s%s\r\n",zContentType,
bAddCharset ? "; charset=utf-8" : "");
if( zEncoding ){
nOut += althttpd_printf("Content-encoding: %s\r\n", zEncoding);
}
nOut += althttpd_printf("Content-length: %d\r\n\r\n",(int)pStat->st_size);
fflush(stdout);
if( strcmp(zMethod,"HEAD")==0 ){
MakeLogEntry(0, 2); /* LOG: Normal HEAD reply */
fclose(in);
fflush(stdout);
return 1;
}
#ifdef linux
if( 2!=useHttps ){
off_t offset = rangeStart;
nOut += sendfile(fileno(stdout), fileno(in), &offset, pStat->st_size);
}else
#endif
{
xferBytes(in, stdout, (int)pStat->st_size, rangeStart);
}
fclose(in);
return 0;
}
stream_file
所谓的流式文件读写
/*
** Streams all contents from in to out. If in TLS mode, the
** output stream is ignored and the output instead goes to the TLS channel.
** 从输入流in中流式传输所有内容到输出流out。如果处于TLS模式,则
** 忽略输出流,而是将输出发送到TLS通道。
*/
static void stream_file(FILE * const in, FILE * const out){
enum { STREAMBUF_SIZE = 1024 * 4 };
char streamBuf[STREAMBUF_SIZE];
size_t n;
while( (n = fread(streamBuf, 1,sizeof(STREAMBUF_SIZE),in)) ){
althttpd_fwrite(streamBuf, 1, n, out);
}
}
CgiHandleReply、SendScgiRequest
/*
** A CGI or SCGI script has run and is sending its reply back across
** the channel "in". Process this reply into an appropriate HTTP reply.
** Close the "in" channel when done.
** 一个CGI或SCGI脚本已经运行,并正在通过通道“in”发送其回复。
** 处理此回复以生成适当的HTTP回复。完成后关闭“in”通道。
**
** If isNPH is true, the input is assumed to be from a
** non-parsed-header CGI and is passed on as-is to stdout or the TLS
** layer, depending on the connection state.
** 如果isNPH为真,假定输入来自未解析头部的CGI,将其原样传递到stdout或TLS
** 层,具体取决于连接状态。
*/
static void CgiHandleReply(FILE *in, int isNPH){
int seenContentLength = 0; /* True if Content-length: header seen 如果看到Content-length:头部则为真*/
int contentLength = 0; /* The content length 内容长度*/
size_t nRes = 0; /* Bytes of payload 载荷的字节数*/
size_t nMalloc = 0; /* Bytes of space allocated to aRes 分配给aRes的空间字节数*/
char *aRes = 0; /* Payload 载荷*/
int c; /* Next character from in 来自in的下一个字符*/
char *z; /* Pointer to something inside of zLine zLine内的某个指针*/
int iStatus = 0; /* Reply status code 回复状态码*/
char zLine[1000]; /* One line of reply from the CGI script 来自CGI脚本的回复的一行*/
/* Set a 1-hour timeout, so that we can implement Hanging-GET or
** long-poll style CGIs. The RLIMIT_CPU will serve as a safety
** to help prevent a run-away CGI
设置1小时的超时,以便我们可以实现Hanging-GET或长轮询样式的CGI。
** RLIMIT_CPU将作为安全机制,帮助防止运行失控的CGI */
SetTimeout(60*60, 800); /* LOG: CGI Handler timeout CGI处理程序超时*/
if( isNPH ){
/*
** Non-parsed-header output: simply pipe it out as-is. We
** need to go through this routine, instead of simply exec()'ing,
** in order to go through the TLS output channel.
** 未解析头部的输出:将其原样管道传输。我们
** 需要通过此例程进行,而不是简单地执行exec(),
** 以便通过TLS输出通道。
*/
stream_file(in, stdout);
fclose(in);
return;
}
while( fgets(zLine,sizeof(zLine),in) && !isspace((unsigned char)zLine[0]) ){
if( strncasecmp(zLine,"Location:",9)==0 ){
StartResponse("302 Redirect");
RemoveNewline(zLine);
z = &zLine[10];
while( isspace(*(unsigned char*)z) ){ z++; }
nOut += althttpd_printf("Location: %s\r\n",z);
rangeEnd = 0;
}else if( strncasecmp(zLine,"Status:",7)==0 ){
int i;
for(i=7; isspace((unsigned char)zLine[i]); i++){}
strncpy(zReplyStatus, &zLine[i], 3);
zReplyStatus[3] = 0;
iStatus = atoi(zReplyStatus);
if( rangeEnd==0 || (iStatus!=200 && iStatus!=206) ){
if( iStatus==418 ){
/* If a CGI returns a status code of 418 ("I'm a teapot", rfc2324)
** that is a signal from the CGI to althttpd that the request was
** abuse - for example an attempted SQL injection attack or
** similar. */
/* 如果CGI返回状态码418("I'm a teapot",rfc2324),
** 这是CGI向althttpd发送的信号,表明请求被滥用 -
** 例如尝试的SQL注入攻击或类似的情况。*/
BlockIPAddress();
ServiceUnavailable(903); /* LOG: CGI reports abuse CGI报告滥用*/
}
nOut += althttpd_printf("%s %s", zProtocol, &zLine[i]);
rangeEnd = 0;
statusSent = 1;
}
}else if( strncasecmp(zLine, "Content-length:", 15)==0 ){
seenContentLength = 1;
contentLength = atoi(zLine+15);
}else{
size_t nLine = strlen(zLine);
if( nRes+nLine >= nMalloc ){
nMalloc += nMalloc + nLine*2;
aRes = realloc(aRes, nMalloc+1);
if( aRes==0 ){
Malfunction(600, "Out of memory: %d bytes", nMalloc); /* LOG: OOM 内存不足*/
}
}
memcpy(aRes+nRes, zLine, nLine);
nRes += nLine;
}
}
/* Copy everything else thru without change or analysis.其余内容直接复制而不更改或分析。
*/
if( rangeEnd>0 && seenContentLength && rangeStart<contentLength ){
StartResponse("206 Partial Content");
if( rangeEnd>=contentLength ){
rangeEnd = contentLength-1;
}
nOut += althttpd_printf("Content-Range: bytes %d-%d/%d\r\n",
rangeStart, rangeEnd, contentLength);
contentLength = rangeEnd + 1 - rangeStart;
}else{
StartResponse("200 OK");
}
if( nRes>0 ){
aRes[nRes] = 0;
althttpd_fwrite(aRes, nRes, 1, stdout);
nOut += nRes;
nRes = 0;
}
if( iStatus==304 ){
nOut += althttpd_printf("\r\n\r\n");
}else if( seenContentLength ){
nOut += althttpd_printf("Content-length: %d\r\n\r\n", contentLength);
xferBytes(in, stdout, contentLength, rangeStart);
}else{
while( (c = getc(in))!=EOF ){
if( nRes>=nMalloc ){
nMalloc = nMalloc*2 + 1000;
aRes = realloc(aRes, nMalloc+1);
if( aRes==0 ){
Malfunction(610, "Out of memory: %d bytes", nMalloc); /* LOG: OOM */
}
}
aRes[nRes++] = c;
}
if( nRes ){
aRes[nRes] = 0;
nOut += althttpd_printf("Content-length: %d\r\n\r\n", (int)nRes);
nOut += althttpd_fwrite(aRes, nRes, 1, stdout);
}else{
nOut += althttpd_printf("Content-length: 0\r\n\r\n");
}
}
free(aRes);
fclose(in);
}
/*
** Send an SCGI request to a host identified by zFile and process the reply.
** 发送一个SCGI请求到由zFile标识的主机,并处理回复。
*/
static void SendScgiRequest(const char *zFile, const char *zScript){
FILE *in;
FILE *s;
char *z;
char *zHost;
char *zPort = 0;
char *zRelight = 0;
char *zFallback = 0;
int rc;
int iSocket = -1;
struct addrinfo hints;
struct addrinfo *ai = 0;
struct addrinfo *p;
char *zHdr;
size_t nHdr = 0;
size_t nHdrAlloc;
int i;
char zLine[1000];
char zExtra[1000];
in = fopen(zFile, "rb");//打开SCGI配置文件
if( in==0 ){
Malfunction(700, "cannot open \"%s\"\n", zFile); /* LOG: cannot open file */
}
if( fgets(zLine, sizeof(zLine)-1, in)==0 ){
Malfunction(701, "cannot read \"%s\"\n", zFile); /* LOG: cannot read file */
}
if( strncmp(zLine,"SCGI ",5)!=0 ){
Malfunction(702, /* LOG: bad SCGI spec */
"misformatted SCGI spec \"%s\"\n", zFile);//SCGI规范格式错误
}
z = zLine+5;
zHost = GetFirstElement(z,&z);
zPort = GetFirstElement(z,0);
if( zHost==0 || zHost[0]==0 || zPort==0 || zPort[0]==0 ){
Malfunction(703, /* LOG: bad SCGI spec (2) */
"misformatted SCGI spec \"%s\"\n", zFile);//SCGI规范格式错误
}
// 读取SCGI配置文件的额外行
while( fgets(zExtra, sizeof(zExtra)-1, in) ){
char *zCmd = GetFirstElement(zExtra,&z);
if( zCmd==0 ) continue;
if( zCmd[0]=='#' ) continue;
RemoveNewline(z);
if( strcmp(zCmd, "relight:")==0 ){ 处理"relight:"指令
free(zRelight);
zRelight = StrDup(z);
continue;
}
if( strcmp(zCmd, "fallback:")==0 ){ 处理"fallback:"指令
free(zFallback);
zFallback = StrDup(z);
continue;
}
Malfunction(704, /* LOG: Unrecognized line in SCGI spec */
"unrecognized line in SCGI spec: \"%s %s\"\n",
zCmd, z ? z : "");//在SCGI规范中无法识别的行
}
fclose(in);// 关闭SCGI配置文件
memset(&hints, 0, sizeof(struct addrinfo));// 初始化地址信息结构体
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
rc = getaddrinfo(zHost,zPort,&hints,&ai);// 解析主机名和端口号
if( rc ){
Malfunction(705, /* LOG: Cannot resolve SCGI server name */
"cannot resolve SCGI server name %s:%s\n%s\n",//无法解析SCGI服务器名
zHost, zPort, gai_strerror(rc));
}
while(1){ /* Exit via break */ // 循环直到成功连接或退出
for(p=ai; p; p=p->ai_next){
iSocket = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if( iSocket<0 ) continue;
if( connect(iSocket,p->ai_addr,p->ai_addrlen)>=0 ) break;
close(iSocket);
}
if( iSocket<0 || (s = fdopen(iSocket,"r+"))==0 ){
if( iSocket>=0 ) close(iSocket);
if( zRelight ){// 处理relight
rc = system(zRelight);
if( rc ){
Malfunction(721, /* LOG: SCGI relight failed */
"Relight failed with %d: \"%s\"\n",
rc, zRelight);
}
free(zRelight);
zRelight = 0;
sleep(1);
continue;
}
if( zFallback ){// 处理fallback
struct stat statbuf;
int rc;
memset(&statbuf, 0, sizeof(statbuf));
if( chdir(zDir) ){// 更改工作目录
char zBuf[1000];
Malfunction(720, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zDir, getcwd(zBuf,999));
}
rc = stat(zFallback, &statbuf);// 检查fallback文件是否存在
if( rc==0 && S_ISREG(statbuf.st_mode) && access(zFallback,R_OK)==0 ){
closeConnection = 1;
rc = SendFile(zFallback, (int)strlen(zFallback), &statbuf);// 发送fallback文件内容
free(zFallback);
althttpd_exit();
}else{
Malfunction(706, /* LOG: bad SCGI fallback */
"bad fallback file: \"%s\"\n", zFallback);//fallback文件错误
}
}
Malfunction(707, /* LOG: Cannot open socket to SCGI 无法打开SCGI服务器套接字*/
"cannot open socket to SCGI server %s\n",
zScript);
}
break;
}
nHdrAlloc = 0;// 分配HTTP头部内存
zHdr = 0;
if( zContentLength==0 ) zContentLength = "0";
ComputeRequestUri();// 计算请求URI
zScgi = "1";
for(i=0; i<(int)(sizeof(cgienv)/sizeof(cgienv[0])); i++){
int n1, n2;
if( cgienv[i].pzEnvValue[0]==0 ) continue;
n1 = (int)strlen(cgienv[i].zEnvName);
n2 = (int)strlen(*cgienv[i].pzEnvValue);
if( n1+n2+2+nHdr >= nHdrAlloc ){
nHdrAlloc = nHdr + n1 + n2 + 1000;
zHdr = realloc(zHdr, nHdrAlloc);
if( zHdr==0 ){
Malfunction(708, "out of memory"); /* LOG: OOM */
}
}
memcpy(zHdr+nHdr, cgienv[i].zEnvName, n1);
nHdr += n1;
zHdr[nHdr++] = 0;
memcpy(zHdr+nHdr, *cgienv[i].pzEnvValue, n2);
nHdr += n2;
zHdr[nHdr++] = 0;
}
zScgi = 0;
fprintf(s,"%d:",(int)nHdr);
fwrite(zHdr, 1, nHdr, s);
fprintf(s,",");
free(zHdr);
if( nPostData>0 ){// 如果有POST数据,则将其写入套接字
size_t wrote = 0;
while( wrote<(size_t)nPostData ){
size_t n = fwrite(zPostData+wrote, 1, nPostData-wrote, s);
if( n<=0 ) break;
wrote += n;
}
free(zPostData);
zPostData = 0;
nPostData = 0;
}
fflush(s);
CgiHandleReply(s, 0);// 处理SCGI回复
}
tls_init_conn tls_close_conn
/*
** If running in builtin TLS mode, initializes the SSL I/O
** state and returns 1, else does nothing and returns 0.
如果运行在内置的TLS模式下,初始化SSL I/O状态并返回1,否则什么都不做并返回0。
*/
static int tls_init_conn(int iSocket){
#ifdef ENABLE_TLS
if( 2==useHttps ){
/*assert(NULL==tlsState.sslCon);*/
if( NULL==tlsState.sslCon ){
tlsState.sslCon = (TlsServerConn *)tls_new_server(iSocket);
if( NULL==tlsState.sslCon ){
Malfunction(512, /* LOG: TLS context */
"Could not instantiate TLS context.");//无法实例化TLS上下文
}
atexit(tls_atexit);
}
return 1;
}
#else
if( 0==iSocket ){/*unused arg*/}
#endif
return 0;
}
static void tls_close_conn(void){//关闭TLS连接。
#ifdef ENABLE_TLS
if( tlsState.sslCon ){
tls_close_server(tlsState.sslCon);
tlsState.sslCon = NULL;
}
#endif
}
DisallowedRemoteAddr
/*
** Check to see if zRemoteAddr is disallowed. Return true if it is disallowed and false if not.
** 检查zRemoteAddr是否被禁止。如果被禁止,则返回true,否则返回false。
**
** zRemoteAddr is disallowed if:如果zRemoteAddr被禁止,满足以下条件:
**
** * The zIPShunDir variable is not NULL, zIPShunDir变量不为NULL
**
** * zIPShunDir is the name of a directory,zIPShunDir是一个目录的名称
**
** * There is a file in zIPShunDir whose name is exactly zRemoteAddr and that is N bytes in size.
在zIPShunDir目录中有一个文件,其名称恰好为zRemoteAddr 并且其大小为N字节。
**
** * N==0 or the mtime of the file is less than N*BANISH_TIME seconds ago.
** * N==0或者文件的修改时间在N*BANISH_TIME秒之前。
**
** If N>0 and the mtime is greater than N*5*BANISH_TIME seconds
** (25 minutes per byte, by default) old, then the file is deleted.
** 如果N>0且修改时间大于N*5*BANISH_TIME秒(默认每字节25分钟),则删除该文件。
**
** The size of the file determines how long the embargo is suppose to
** last. A zero-byte file embargos forever. Otherwise, the embargo
** is for BANISH_TIME bytes for each byte in the file.
** 文件的大小决定了禁令应该持续多长时间。大小为零的文件永远被禁止。否则,禁令
** 将持续BANISH_TIME字节乘以文件中的每个字节的时间。
*/
static int DisallowedRemoteAddr(void){
char zFullname[1000];
size_t nIPShunDir;
size_t nRemoteAddr;
int rc;
struct stat statbuf;
time_t now;
if( zIPShunDir==0 ) return 0;
if( zRemoteAddr==0 ) return 0;
if( zIPShunDir[0]!='/' ){
Malfunction(910, /* LOG: argument to --ipshun should be absolute path, --ipshun参数应该是绝对路径*/
"The --ipshun directory should have an absolute path");
}
nIPShunDir = strlen(zIPShunDir);
while( nIPShunDir>0 && zIPShunDir[nIPShunDir-1]=='/' ) nIPShunDir--;
nRemoteAddr = strlen(zRemoteAddr);
if( nIPShunDir + nRemoteAddr + 2 >= sizeof(zFullname) ){
Malfunction(912, /* LOG: RemoteAddr filename too big, RemoteAddr文件名太大*/
"RemoteAddr filename too big");
}
if( zRemoteAddr[0]==0
|| zRemoteAddr[0]=='.'
|| strchr(zRemoteAddr,'/')!=0
){
Malfunction(913, /* LOG: RemoteAddr contains suspicious characters,RemoteAddr包含可疑字符 */
"RemoteAddr contains suspicious characters");
}
memcpy(zFullname, zIPShunDir, nIPShunDir);
zFullname[nIPShunDir] = '/';
memcpy(zFullname+nIPShunDir+1, zRemoteAddr, nRemoteAddr+1);
memset(&statbuf, 0, sizeof(statbuf));
rc = stat(zFullname, &statbuf);
if( rc ) return 0; /* No such file, hence no restrictions ,没有这个文件,因此没有限制*/
if( statbuf.st_size==0 ) return 1; /* Permanently banned ,永久禁止*/
time(&now);
if( statbuf.st_size*BANISH_TIME + statbuf.st_mtime >= now ){
return 1; /* Currently under a ban , 当前处于禁令状态*/
}
if( statbuf.st_size*5*BANISH_TIME + statbuf.st_mtime < now ){
unlink(zFullname);
}
return 0;
}
ProcessOneRequest
处理单个HTTP请求
/*
** This routine processes a single HTTP request on standard input and
** sends the reply to standard output. If the argument is 1 it means
** that we are should close the socket without processing additional
** HTTP requests after the current request finishes. 0 means we are
** allowed to keep the connection open and to process additional requests.
** This routine may choose to close the connection even if the argument is 0.
该例程处理标准输入上的单个HTTP请求,并将回复发送到标准输出。
如果参数是1,表示在当前请求完成后应关闭套接字而不处理其他HTTP请求;
如果是0,则允许保持连接并处理其他请求。即使参数为0,此例程也可能选择关闭连接。
**
** If the connection should be closed, this routine calls exit() and
** thus never returns. If this routine does return it means that another
** HTTP request may appear on the wire.
如果应关闭连接,该例程调用exit()并因此永远不返回。
如果此例程返回,意味着可能会在连接中出现另一个HTTP请求。
**
** socketId must be 0 (if running via xinetd/etc) or the socket ID
** accept()ed by http_server(). It is only used for built-in TLS mode.
socketId必须为0(如果通过xinetd等方式运行)或由http_server()接受的accept()的套接字ID。
仅在内置TLS模式下使用。
*/
void ProcessOneRequest(int forceClose, int socketId){
int i, j, j0;
char *z; /* Used to parse up a string 用于解析字符串 */
struct stat statbuf; /* Information about the file to be retrieved 关于要检索的文件的信息*/
FILE *in; /* For reading from CGI scripts 用于从CGI脚本中读取*/
#ifdef LOG_HEADER
FILE *hdrLog = 0; /* Log file for complete header content 完整标头内容的日志文件*/
#endif
char zLine[10000]; /* A buffer for input lines or forming names */
const MimeTypeDef *pMimeType = 0; /* URI's mimetype URI的MIME类型*/
size_t sz = 0;
struct tm vTm; /* Timestamp for zExpLogFile */
if( zLogFile ){
assert(beginTime.tv_sec > 0);
gmtime_r(&beginTime.tv_sec, &vTm);
sz = strftime(zExpLogFile, sizeof(zExpLogFile), zLogFile, &vTm);
}
if( sz==0 || sz>=sizeof(zExpLogFile)-2 ){
/* Invalid zExpLogFile name 无效的zExpLogFile名称*/
zExpLogFile[0] = 0;
}
/* Must see a header within 10 seconds for the first request.
** Allow up to 5 more minutes for the follow-on requests
在前10秒内必须看到第一个请求的标头。 对于后续的请求,最多允许额外5分钟
*/
if( useTimeout ){
if( nRequest>0 ){
SetTimeout(60*5, 801); /* LOG: Timeout request header (1+) */
}else{
SetTimeout(10, 802); /* LOG: Timeout request header (0) */
}
}
/* Change directories to the root of the HTTP filesystem
更改目录到HTTP文件系统的根目录 */
if( chdir(zRoot[0] ? zRoot : "/")!=0 ){
char zBuf[1000];
Malfunction(190, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zRoot, getcwd(zBuf,sizeof(zBuf)-1));
}
nRequest++;
tls_init_conn(socketId);
/* Get the first line of the request and parse out the method, the script and the protocol.
获取请求的第一行并解析方法、脚本和协议
*/
omitLog = 1;
if( althttpd_fgets(zLine,sizeof(zLine),stdin)==0 ){
exit(0);
}
clock_gettime(ALTHTTPD_CLOCK_ID, &tsBeginTime);
gettimeofday(&beginTime, 0);
omitLog = 0;
nIn += (i = (int)strlen(zLine));
/* Parse the first line of the HTTP request 解析HTTP请求的第一行*/
zMethod = StrDup(GetFirstElement(zLine,&z));
zRealScript = zScript = StrDup(GetFirstElement(z,&z));
zProtocol = StrDup(GetFirstElement(z,&z));
if( zProtocol==0
|| strncmp(zProtocol,"HTTP/",5)!=0
|| strlen(zProtocol)!=8
|| i>9990
){
zProtocol = 0;
if( i<=9990 ){
StartResponse("400 Bad Request");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"This server does not understand the requested protocol\n"//此服务器无法理解请求的协议
);
MakeLogEntry(0, 200); /* LOG: bad protocol in HTTP header 请求头中的错误协议*/
}else{
StartResponse("414 URI Too Long");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"URI too long\n"
);
MakeLogEntry(0, 201); /* LOG: bad protocol in HTTP header 请求头中的错误协议*/
}
althttpd_exit();
}
if( zScript[0]!='/' ) NotFound(210); /* LOG: Empty request URI */
while( zScript[1]=='/' ){
zScript++;
zRealScript++;
}
if( forceClose ){
closeConnection = 1;
}else if( zProtocol[5]<'1' || zProtocol[7]<'1' ){
closeConnection = 1;
}
/* This very simple server only understands the GET, POST and HEAD methods
这个非常简单的服务器只理解GET、POST和HEAD方法 */
if( strcmp(zMethod,"GET")!=0 && strcmp(zMethod,"POST")!=0
&& strcmp(zMethod,"HEAD")!=0 ){
StartResponse("501 Not Implemented");//如果方法不是GET、POST或HEAD,则响应"501 Not Implemented"
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"The %s method is not implemented on this server.\n",
zMethod);
MakeLogEntry(0, 220); /* LOG: Unknown request method */
althttpd_exit();
}
/* If there is a log file (if zLogFile!=0) and if the pathname in
** the first line of the http request contains the magic string
** "FullHeaderLog" then write the complete header text into the
** file %s(zLogFile)-hdr. Overwrite the file. This is for protocol
** debugging only and is only enabled if althttpd is compiled with
** the -DLOG_HEADER=1 option.
如果存在日志文件(如果zLogFile!=0),并且HTTP请求的第一行中的路径名包含魔术字符串
"FullHeaderLog",则将完整的标头文本写入文件%s(zLogFile)-hdr。覆盖文件。
这仅用于协议调试,仅在编译althttpd时启用了-DLOG_HEADER=1选项时才有效。
*/
#ifdef LOG_HEADER
if( zLogFile
&& strstr(zScript,"FullHeaderLog")!=0
&& strlen(zLogFile)<sizeof(zLine)-50
){
sprintf(zLine, "%s-hdr", zLogFile);
hdrLog = fopen(zLine, "wb");
}
#endif
/* Get all the optional fields that follow the first line.
获取跟在第一行后面的所有可选字段 */
zCookie = 0;
zAuthType = 0;
zRemoteUser = 0;
zReferer = 0;
zIfNoneMatch = 0;
zIfModifiedSince = 0;
zContentLength = 0;
rangeEnd = 0;
while( althttpd_fgets(zLine,sizeof(zLine),stdin) ){
char *zFieldName;
char *zVal;
#ifdef LOG_HEADER
if( hdrLog ) fprintf(hdrLog, "%s", zLine);
#endif
nIn += strlen(zLine);
zFieldName = GetFirstElement(zLine,&zVal);
if( zFieldName==0 || *zFieldName==0 ) break;
RemoveNewline(zVal);
if( strcasecmp(zFieldName,"User-Agent:")==0 ){
zAgent = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Accept:")==0 ){
zAccept = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Accept-Encoding:")==0 ){
zAcceptEncoding = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Content-length:")==0 ){
zContentLength = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Content-type:")==0 ){
zContentType = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Referer:")==0 ){
zReferer = StrDup(zVal);
if( strstr(zVal, "devids.net/")!=0 ){ zReferer = "devids.net.smut";
Forbidden(230); /* LOG: Referrer is devids.net */
}
}else if( strcasecmp(zFieldName,"Cookie:")==0 ){
zCookie = StrAppend(zCookie,"; ",zVal);
}else if( strcasecmp(zFieldName,"Connection:")==0 ){
if( strcasecmp(zVal,"close")==0 ){
closeConnection = 1;
}else if( !forceClose && strcasecmp(zVal, "keep-alive")==0 ){
closeConnection = 0;
}
}else if( strcasecmp(zFieldName,"Host:")==0 ){
int inSquare = 0;
char c;
if( sanitizeString(zVal) ){
Forbidden(240); /* LOG: Illegal content in HOST: parameter */
}
zHttpHost = StrDup(zVal);
zServerPort = zServerName = StrDup(zHttpHost);
while( zServerPort && (c = *zServerPort)!=0
&& (c!=':' || inSquare) ){
if( c=='[' ) inSquare = 1;
if( c==']' ) inSquare = 0;
zServerPort++;
}
if( zServerPort && *zServerPort ){
*zServerPort = 0;
zServerPort++;
}
if( zRealPort ){
zServerPort = StrDup(zRealPort);
}
}else if( strcasecmp(zFieldName,"Authorization:")==0 ){
zAuthType = GetFirstElement(StrDup(zVal), &zAuthArg);
}else if( strcasecmp(zFieldName,"If-None-Match:")==0 ){
zIfNoneMatch = StrDup(zVal);
}else if( strcasecmp(zFieldName,"If-Modified-Since:")==0 ){
zIfModifiedSince = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Range:")==0
&& strcmp(zMethod,"GET")==0 ){
int x1 = 0, x2 = 0;
int n = sscanf(zVal, "bytes=%d-%d", &x1, &x2);
if( n==2 && x1>=0 && x2>=x1 ){
rangeStart = x1;
rangeEnd = x2;
}else if( n==1 && x1>0 ){
rangeStart = x1;
rangeEnd = 0x7fffffff;
}
}
}
#ifdef LOG_HEADER
if( hdrLog ) fclose(hdrLog);
#endif
/* Disallow requests from certain clients 禁止来自特定客户端的请求*/
if( zAgent ){
const char *azDisallow[] = {
"Amazonbot",
"Windows 9",
"Download Master",
"Ezooms/",
"DotBot",
"HTTrace",
"AhrefsBot",
"MicroMessenger",
"OPPO A33 Build",
"SemrushBot",
"MegaIndex.ru",
"MJ12bot",
"Chrome/0.A.B.C",
"Neevabot/",
"BLEXBot/",
"Synapse",
};
size_t ii;
for(ii=0; ii<sizeof(azDisallow)/sizeof(azDisallow[0]); ii++){
if( strstr(zAgent,azDisallow[ii])!=0 ){
Forbidden(250); /* LOG: Disallowed user agent 禁止用户*/
}
}
#if 0
/* Spider attack from 2019-04-24 */
if( strcmp(zAgent,
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36")==0 ){
Forbidden(251); /* LOG: Disallowed user agent (20190424) */
}
#endif
}
#if 0
if( zReferer ){
static const char *azDisallow[] = {
"skidrowcrack.com",
"hoshiyuugi.tistory.com",
"skidrowgames.net",
};
int i;
for(i=0; i<sizeof(azDisallow)/sizeof(azDisallow[0]); i++){
if( strstr(zReferer, azDisallow[i])!=0 ){
NotFound(260); /* LOG: Disallowed referrer */
}
}
}
#endif
/* Make an extra effort to get a valid server name and port number.
** Only Netscape provides this information. If the browser is
** Internet Explorer, then we have to find out the information for ourselves.
进一步努力获取有效的服务器名称和端口号。
** 只有 Netscape 提供此信息。如果浏览器是
** Internet Explorer,则我们必须自己找到这些信息。
*/
if( zServerName==0 ){
zServerName = SafeMalloc( 100 );
gethostname(zServerName,100);
}
if( zServerPort==0 || *zServerPort==0 ){
zServerPort = DEFAULT_PORT;
}
/* Remove the query string from the end of the requested file.
从请求的文件末尾删除查询字符串*/
for(z=zScript; *z && *z!='?'; z++){}
if( *z=='?' ){
zQuerySuffix = StrDup(z);
*z = 0;
if( (zReferer==0 || zReferer[0]==0)
&& strncmp(zQuerySuffix+1, "w=%2", 4)==0
){
NotFound(261); /* LOG: Spider attack */
}
}else{
zQuerySuffix = "";
}
zQueryString = *zQuerySuffix ? &zQuerySuffix[1] : zQuerySuffix;
/* Create either a memory buffer to hold the POST query data */
if( zMethod[0]=='P' && zContentLength!=0 ){
size_t len = atoi(zContentLength);
if( len>MAX_CONTENT_LENGTH ){
StartResponse("500 Request too large");
nOut += althttpd_printf(
"Content-type: text/plain; charset=utf-8\r\n"
"\r\n"
"Too much POST data\n"
);
MakeLogEntry(0, 270); /* LOG: Request too large */
althttpd_exit();
}
rangeEnd = 0;
zPostData = SafeMalloc( len+1 );
SetTimeout(15 + len/2000, 803); /* LOG: Timeout POST data */
nPostData = althttpd_fread(zPostData,1,len,stdin);
nIn += nPostData;
}
/* Make sure the running time is not too great 设置超时*/
SetTimeout(30, 804); /* LOG: Timeout decode HTTP request */
/* Convert all unusual characters in the script name into "_".
** This is a defense against various attacks, XSS attacks in particular.
消除非法字符*/
sanitizeString(zScript);
/* Refuse to process the request if the IP address has been banished
如果IP地址被列入禁用列表,则拒绝处理请求*/
if( zIPShunDir && DisallowedRemoteAddr() ){
ServiceUnavailable(901); /* LOG: Prohibited remote IP address */
}
/* Do not allow "/." or "/-" to to occur anywhere in the entity name.
** This prevents attacks involving ".." and also allows us to create
** files and directories whose names begin with "-" or "." which are
** invisible to the webserver.
**在实体名称的任何位置都不允许出现 "/." 或 "/-"。
** 这可以防止涉及 ".." 的攻击,并允许我们创建以 "-" 或 "." 开头的文件和目录,
** 这些文件和目录对Web服务器不可见。
** Exception: Allow the "/.well-known/" prefix in accordance with RFC-5785.
** 例外情况:根据RFC-5785允许使用 "/.well-known/" 前缀。
*/
for(z=zScript; *z; z++){
if( *z=='/' && (z[1]=='.' || z[1]=='-') ){
if( strncmp(zScript,"/.well-known/",13)==0 && (z[1]!='.' || z[2]!='.') ){
/* Exception: Allow "/." and "/-" for URLs that being with
** "/.well-known/". But do not allow "/..". */
continue;
}
NotFound(300); /* LOG: Path element begins with "." or "-" */
}
}
/* Figure out what the root of the filesystem should be. If the
** HTTP_HOST parameter exists (stored in zHttpHost) then remove the
** port number from the end (if any), convert all characters to lower
** case, and convert non-alphanumber characters (including ".") to "_".
** Then try to find a directory with that name and the extension .website.
** If not found, look for "default.website".
确定文件系统的根目录应该是什么。如果存在HTTP_HOST参数(存储在zHttpHost中),
** 那么从末尾删除端口号(如果有的话),将所有字符转换为小写,并将非字母数字字符(包括 ".")转换为“”。
** 然后尝试查找具有该名称和扩展名.website的目录。如果找不到,则查找"default.website"。
*/
if( zScript[0]!='/' ){
NotFound(310); /* LOG: URI does not start with "/" */
}
if( strlen(zRoot)+40 >= sizeof(zLine) ){
NotFound(320); /* LOG: URI too long */
}
if( zHttpHost==0 || zHttpHost[0]==0 ){
NotFound(330); /* LOG: Missing HOST: parameter */
}else if( strlen(zHttpHost)+strlen(zRoot)+10 >= sizeof(zLine) ){
NotFound(340); /* LOG: HOST parameter too long */
}else{
sprintf(zLine, "%s/%s", zRoot, zHttpHost);
for(i=strlen(zRoot)+1; zLine[i] && zLine[i]!=':'; i++){
unsigned char c = (unsigned char)zLine[i];
if( !isalnum(c) ){
if( c=='.' && (zLine[i+1]==0 || zLine[i+1]==':') ){
/* If the client sent a FQDN with a "." at the end
** (example: "sqlite.org." instead of just "sqlite.org") then
** omit the final "." from the document root directory name */
break;
}
zLine[i] = '_';
}else if( isupper(c) ){
zLine[i] = tolower(c);
}
}
strcpy(&zLine[i], ".website");
}
if( stat(zLine,&statbuf) || !S_ISDIR(statbuf.st_mode) ){
sprintf(zLine, "%s/default.website", zRoot);
if( stat(zLine,&statbuf) || !S_ISDIR(statbuf.st_mode) ){
if( standalone ){
sprintf(zLine, "%s", zRoot);
}else{
NotFound(350); /* LOG: *.website permissions */
}
}
}
zHome = StrDup(zLine);
/* Change directories to the root of the HTTP filesystem
切换到HTTP文件系统的根目录*/
if( chdir(zHome)!=0 ){
char zBuf[1000];
Malfunction(360, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zHome, getcwd(zBuf,999));
}
/* Locate the file in the filesystem. We might have to append
** a name like "/home" or "/index.html" or "/index.cgi" in order
** to find it. Any excess path information is put into the
** zPathInfo variable.
在文件系统中定位文件。为了找到文件,我们可能需要附加类似于"/home"、"/index.html"或"/index.cgi"的名称。
任何多余的路径信息都会存储在变量zPathInfo中。
*/
j = j0 = (int)strlen(zLine);
i = 0;
while( zScript[i] ){
while( zScript[i] && (i==0 || zScript[i]!='/') ){
zLine[j] = zScript[i];
i++; j++;
}
zLine[j] = 0;
/* fprintf(stderr, "searching [%s]...\n", zLine); */
if( stat(zLine,&statbuf)!=0 ){
int stillSearching = 1;
while( stillSearching && i>0 && j>j0 ){
while( j>j0 && zLine[j-1]!='/' ){ j--; }
strcpy(&zLine[j-1], "/not-found.html");
if( stat(zLine,&statbuf)==0 && S_ISREG(statbuf.st_mode)
&& access(zLine,R_OK)==0 ){
zRealScript = StrDup(&zLine[j0]);
Redirect(zRealScript, 302, 1, 370); /* LOG: redirect to not-found */
return;
}else{
j--;
}
}
if( stillSearching ) NotFound(380); /* LOG: URI not found */
break;
}
if( S_ISREG(statbuf.st_mode) ){
if( access(zLine,R_OK) ){
NotFound(390); /* LOG: File not readable */
}
zRealScript = StrDup(&zLine[j0]);
break;
}
if( zScript[i]==0 || zScript[i+1]==0 ){
static const char *azIndex[] = {
"/home", "/index", "/index.html", "/index.cgi"
};
int k = j>0 && zLine[j-1]=='/' ? j-1 : j;
unsigned int jj;
for(jj=0; jj<sizeof(azIndex)/sizeof(azIndex[0]); jj++){
strcpy(&zLine[k],azIndex[jj]);
if( stat(zLine,&statbuf)!=0 ) continue;
if( !S_ISREG(statbuf.st_mode) ) continue;
if( access(zLine,R_OK) ) continue;
break;
}
if( jj>=sizeof(azIndex)/sizeof(azIndex[0]) ){
NotFound(400); /* LOG: URI is a directory w/o index.html */
}
zRealScript = StrDup(&zLine[j0]);
if( zScript[i]==0 ){
/* If the requested URL does not end with "/" but we had to
** append "index.html", then a redirect is necessary. Otherwise
** none of the relative URLs in the delivered document will be correct.
** 如果请求的URL不以"/"结尾,但我们不得不附加"index.html",则需要进行重定向。
否则,交付文档中的相对URL将全部不正确。*/
Redirect(zRealScript,301,1,410); /* LOG: redirect to add trailing / */
return;
}
break;
}
zLine[j] = zScript[i];
i++; j++;
}
zFile = StrDup(zLine);
zPathInfo = StrDup(&zScript[i]);
lenFile = strlen(zFile);
zDir = StrDup(zFile);
for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){};
if( i==0 ){
strcpy(zDir,"/");
}else{
zDir[i] = 0;
}
/* Check to see if there is an authorization file. If there is, process it.
检查是否存在授权文件。如果有,进行处理。*/
sprintf(zLine, "%s/-auth", zDir);
if( access(zLine,R_OK)==0 && !CheckBasicAuthorization(zLine) ){
tls_close_conn();
return;
}
/* Take appropriate action 采取适当的行动。 */
if( (statbuf.st_mode & 0100)==0100 && access(zFile,X_OK)==0
&& (!(pMimeType = GetMimeType(zFile, lenFile))
|| 0==(pMimeType->flags & MTF_NOCGI)) ){ /* CGI */
char *zBaseFilename; /* Filename without directory prefix */
int px[2]; /* CGI-1 to althttpd pipe */
int py[2]; /* zPostData to CGI-0 pipe */
/* Abort with an error if the CGI script is writable by anyone other than its owner.
如果 CGI 脚本可被除所有者以外的任何人写入,则中止并显示错误。*/
if( statbuf.st_mode & 0022 ){
CgiScriptWritable();
}
/* Compute the base filename of the CGI script 计算 CGI 脚本的基本文件名。*/
for(i=strlen(zFile)-1; i>=0 && zFile[i]!='/'; i--){}
zBaseFilename = &zFile[i+1];
/* Create pipes used to communicate with the child CGI process 创建用于与子 CGI 进程通信的管道。*/
if( pipe(px) ){
Malfunction(440, /* LOG: pipe() failed */
"Unable to create a pipe for the CGI program");
}
if( pipe(py) ){
Malfunction(441, /* LOG: pipe() failed */
"Unable to create a pipe for the CGI program");
}
/* Create the child process that will run the CGI. 创建将运行 CGI 的子进程。*/
if( fork()==0 ){
/* This code is run by the child CGI process only Begin by setting up the CGI-to-althttpd pipe
此代码仅由子 CGI 进程运行。首先建立 CGI 到 althttpd 的管道。 */
close(1);
if( dup(px[1])<0 ){
Malfunction(442, /* LOG: dup() failed */
"CGI cannot dup() to file descriptor 1");
}
/* Set up the althttpd-to-CGI link */
close(0);
if( dup(py[0])<0 ){
Malfunction(444, /* LOG: dup() failed */
"CGI cannot dup() to file descriptor 0");
}
/* Close all surplus file descriptors */
for(i=3; close(i)==0; i++){}
/* Move into the directory holding the CGI program */
if( chdir(zDir) ){
char zBuf[1000];
Malfunction(445, /* LOG: chdir() failed */
"CGI cannot chdir to [%s] from [%s]",
zDir, getcwd(zBuf,999));
}
/* Setup the CGI environment appropriately. */
ComputeRequestUri();
putenv("GATEWAY_INTERFACE=CGI/1.0");
for(i=0; i<(int)(sizeof(cgienv)/sizeof(cgienv[0])); i++){
if( *cgienv[i].pzEnvValue ){
SetEnv(cgienv[i].zEnvName,*cgienv[i].pzEnvValue);
}
}
/* Run the CGI program */
execl(zBaseFilename, zBaseFilename, (char*)0);
exit(0); /* Not reached */
}
/* This parent process. The child has been started.
** Set up the CGI-to-althttp pipe on which to receive the reply
这是父进程。子进程已经启动。 建立 CGI 到 althttp 的管道,用于接收回复。
*/
close(px[1]);
in = fdopen(px[0], "rb");
/* Set up the althttp-to-CGI pipe used to send POST data (if any)
建立 althttp 到 CGI 的管道,用于发送 POST 数据(如果有的话)。*/
close(py[0]);
if( nPostData>0 ){
ssize_t wrote = 0, n;
while( nPostData>wrote ){
n = write(py[1], zPostData+wrote, nPostData-wrote);
if( n<=0 ) break;
wrote += n;
}
}
if( zPostData ){
free(zPostData);
zPostData = 0;
nPostData = 0;
}
close(py[1]);
/* Wait for the CGI program to reply and process that reply
等待 CGI 程序回复,并处理该回复。 */
if( in==0 ){
CgiError();
}else{
CgiHandleReply(in, strncmp(zBaseFilename,"nph-",4)==0);
}
}else if( lenFile>5 && strcmp(&zFile[lenFile-5],".scgi")==0 ){
/* Any file that ends with ".scgi" is assumed to be text of the form:
任何以“.scgi”结尾的文件都被假定为以下形式的文本:
** SCGI hostname port
** Open a TCP/IP connection to that host and send it an SCGI request
打开到该主机的TCP/IP连接并向其发送一个SCGI请求。
*/
SendScgiRequest(zFile, zScript);
}else if( countSlashes(zRealScript)!=countSlashes(zScript) ){
/* If the request URI for static content contains material past the
** actual content file name, report that as a 404 error.
如果用于静态内容的请求URI包含超出实际内容文件名的部分,请将其报告为404错误。*/
NotFound(460); /* LOG: Excess URI content past static file name 超出静态文件名的URI内容过多*/
}else{
/* If it isn't executable then it must be a simple file that needs to be copied to output.
如果不是可执行文件,那么它必须是一个需要复制到输出的简单文件。*/
SetTimeout(30 + statbuf.st_size/2000,
805); /* LOG: Timeout send static file */
if( SendFile(zFile, lenFile, &statbuf) ){
return;
}
}
//完成静态文件处理或CGI处理后,刷新输出并生成日志
althttpd_fflush(stdout);
MakeLogEntry(0, 0); /* LOG: Normal reply */
omitLog = 1;
}
launch_web_browser
/* Launch a web-browser pointing to zPage
启动一个指向特定网页的网页浏览器*/
static void launch_web_browser(const char *zPath, int iPort){
char zUrl[2000];
static const char *const azBrowserProg[] = {
#if defined(__DARWIN__) || defined(__APPLE__) || defined(__HAIKU__)
"open"
#else
"xdg-open", "gnome-open", "firefox", "google-chrome"
#endif
};
size_t i;
if( strlen(zPath)<=sizeof(zUrl)-1000 ){
while( zPath[0]=='/' ) zPath++;
sprintf(zUrl, "http://localhost:%d/%s", iPort, zPath);
for(i=0; i<sizeof(azBrowserProg)/sizeof(azBrowserProg[0]); i++){
execlp(azBrowserProg[i], azBrowserProg[i], zUrl, (char*)0);
}
}
exit(1);
}
http_server
HTTP服务器守护进程,监听指定范围内的端口,处理HTTP请求。
#define MAX_PARALLEL 50 /* Number of simultaneous children 同时处理的子进程的最大数量*/
/* All possible forms of an IP address. Needed to work around GCC strict aliasing rules.
多种形式的IP地址结构*/
typedef union {
struct sockaddr sa; /* Abstract superclass */
struct sockaddr_in sa4; /* IPv4 */
struct sockaddr_in6 sa6; /* IPv6 */
struct sockaddr_storage sas; /* Should be the maximum of the above 3 */
} address;
/*
** Implement an HTTP server daemon listening on port zPort.
** HTTP服务器守护进程
** As new connections arrive, fork a child and let the child return
** out of this procedure call. The child will handle the request.
** The parent never returns from this procedure.
**
** Return 0 to each child as it runs. If unable to establish a
** listening socket, return non-zero.
**
** When it accept()s a connection, the socket ID is written to the
** final argument.
*/
int http_server(
int mnPort, int mxPort, /* Range of TCP ports to try 最小端口号mnPort、最大端口号mxPort*/
int bLocalhost, /* Listen on loopback sockets only 是否仅监听本地回环地址的标志bLocalhost*/
const char *zPage, /* Launch web browser on this document 要启动的网页路径zPage*/
int *httpConnection /* Socket over which HTTP request arrives, HTTP请求到达的套接字httpConnection*/
){
int listener = -1; /* The server socket 服务器套接字的文件描述符*/
int connection; /* A socket for each individual connection 每个单独连接的套接字的文件描述符*/
fd_set readfds; /* Set of file descriptors for select() 文件描述符集合readfds,用于select()系统调用*/
socklen_t lenaddr; /* Length of the inaddr structure 保存地址结构的长度。*/
int child; /* PID of the child process 子进程的进程ID*/
int nchildren = 0; /* Number of child processes 当前活动的子进程数量。*/
struct timeval delay; /* How long to wait inside select() select()函数的超时时间*/
struct sockaddr_in inaddr; /* The socket address IPv4套接字地址*/
int opt = 1; /* setsockopt flag 套接字选项*/
int iPort = mnPort;
while( iPort<=mxPort ){
memset(&inaddr, 0, sizeof(inaddr));
inaddr.sin_family = AF_INET;
if( bLocalhost ){
inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
}else{
inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
inaddr.sin_port = htons(iPort);
listener = socket(AF_INET, SOCK_STREAM, 0);//监听连接请求,并打印监听的端口信息。
if( listener<0 ){
iPort++;
continue;
}
/* if we can't terminate nicely, at least allow the socket to be reused */
setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if( bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr))<0 ){
close(listener);
iPort++;
continue;
}
break;
}
if( iPort>mxPort ){
if( mnPort==mxPort ){
fprintf(stderr,"unable to open listening socket on port %d\n", mnPort);
}else{
fprintf(stderr,"unable to open listening socket on any"
" port in the range %d..%d\n", mnPort, mxPort);
}
exit(1);
}
if( iPort>mxPort ) return 1;
listen(listener,10);
printf("Listening for %s requests on TCP port %d\n",
useHttps?"TLS-encrypted HTTPS":"HTTP", iPort);
fflush(stdout);
//如果指定了要打开的网页路径zPage,则创建一个子进程,并在子进程中调用launch_web_browser()函数打开网页。
if( zPage ){
child = fork();
if( child!=0 ){
if( child>0 ) nchildren++;
}else{
launch_web_browser(zPage, iPort);
/* NOT REACHED */
exit(1);
}
}
while( 1 ){
if( nchildren>MAX_PARALLEL ){//子进程数量超过了最大并行处理数量MAX_PARALLEL,则主进程会等待一段时间。
/* Slow down if connections are arriving too fast */
sleep( nchildren-MAX_PARALLEL );
}
delay.tv_sec = 0;
delay.tv_usec = 100000;
FD_ZERO(&readfds);
assert( listener>=0 );
FD_SET( listener, &readfds);
select( listener+1, &readfds, 0, 0, &delay);//使用select()函数监听套接字上是否有新的连接请求
if( FD_ISSET(listener, &readfds) ){//接受连接并处理请求
lenaddr = sizeof(inaddr);
connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);
if( connection>=0 ){
child = fork();
if( child!=0 ){
if( child>0 ) nchildren++;
close(connection);
/* printf("subprocess %d started...\n", child); fflush(stdout); */
}else{
int nErr = 0, fd;
close(0);
fd = dup(connection);
if( fd!=0 ) nErr++;
close(1);
fd = dup(connection);
if( fd!=1 ) nErr++;
close(connection);
*httpConnection = fd;
return nErr;
}
}
}
/* Bury dead children */
while( (child = waitpid(0, 0, WNOHANG))>0 ){
/* printf("process %d ends\n", child); fflush(stdout); */
nchildren--;
}
}
/* NOT REACHED 永远不会被执行到*/
exit(1);
}
main
int main(int argc, const char **argv){
int i; /* Loop counter */
const char *zPermUser = 0; /* Run daemon with this user's permissions 记录要以哪个用户的权限运行守护进程。*/
int mnPort = 0; /* Range of TCP ports for server mode, TCP端口范围*/
int mxPort = 0;
int useChrootJail = 1; /* True to use a change-root jail 是否使用chroot环境*/
struct passwd *pwd = 0; /* Information about the user 保存关于用户的信息*/
int httpConnection = 0; /* Socket ID of inbound http connection 入站HTTP连接的套接字ID*/
int bLocalhost = 0; /* Bind to loop-back TCP ports only 是否仅绑定到本地回环TCP端口*/
const char *zPage = 0; /* Starting page 要启动的起始网页*/
/* Record the time when processing begins. */
gettimeofday(&beginTime, 0);
/* Parse command-line arguments 命令行解析*/
while( argc>1 && argv[1][0]=='-' ){
int nTerm = 2;
const char *z = argv[1];
const char *zArg = argc>=3 ? argv[2] : "0";
if( z[0]=='-' && z[1]=='-' ) z++;
if( strcmp(z,"-root")==0 ){
zRoot = zArg;
}else
if( strcmp(z,"-logfile")==0 ){
zLogFile = zArg;
}else
#ifdef ENABLE_TLS
if( strcmp(z, "-cert")==0 ){
useHttps = 2;
zHttpScheme = "https";
zHttps = "on";
tlsState.zCertFile = zArg;
if( tlsState.zKeyFile==0 ) tlsState.zKeyFile = zArg;
if( standalone ){
standalone = 2;
}
}else
if( strcmp(z, "-pkey")==0 ){
tlsState.zKeyFile = zArg;
}else
#endif
if( strcmp(z,"-user")==0 ){
zPermUser = zArg;
}else
if( strcmp(z,"-ipshun")==0 ){
zIPShunDir = zArg;
}else
if( strcmp(z,"-max-age")==0 ){
mxAge = atoi(zArg);
}else
if( strcmp(z,"-max-cpu")==0 ){
maxCpu = atoi(zArg);
}else
if( strcmp(z,"-loopback")==0 ){
bLocalhost = 1;
nTerm = 1;
}else
if( strcmp(z,"-enable-sab")==0 ){
enableSAB = 1;
}else
if( strcmp(z,"-page")==0 ){
zPage = zArg;
bLocalhost = 1;
if( mnPort==0 ){
mnPort = 8080;
mxPort = 8100;
}
standalone = 1 + (useHttps==2);
}else
if( strcmp(z,"-popup")==0 ){
nTerm = 1;
zRoot = ".";
if( mnPort==0 ){
mnPort = 8080;
mxPort = 8100;
}
standalone = 1 + (useHttps==2);
}else
if( strcmp(z,"-https")==0 ){
int const x = atoi(zArg);
if( x<=0 ){
useHttps = 0;
zHttpScheme = "http";
zHttps = 0;
}else{
zHttpScheme = "https";
zHttps = "on";
zRemoteAddr = getenv("REMOTE_HOST");
useHttps = 1;
}
}else
if( strcmp(z, "-port")==0 ){
int ii;
mnPort = mxPort = 0;
for(ii=0; zArg[ii]>='0' && zArg[ii]<='9'; ii++){
mnPort = mnPort*10 + zArg[ii] - '0';
}
if( zArg[ii]==0 ){
mxPort = mnPort;
}else if( zArg[ii]=='.' && zArg[ii+1]=='.' ){
for(ii+=2; zArg[ii]>='0' && zArg[ii]<='9'; ii++){
mxPort = mxPort*10 + zArg[ii] - '0';
}
}
standalone = 1 + (useHttps==2);
}else
if( strcmp(z, "-family")==0 ){
if( strcmp(zArg, "ipv4")==0 ){
ipv4Only = 1;
}else if( strcmp(zArg, "ipv6")==0 ){
ipv6Only = 1;
}else{
Malfunction(513, /* LOG: unknown IP protocol */
"unknown IP protocol: [%s]\n", zArg);
}
}else
if( strcmp(z, "-jail")==0 ){
if( atoi(zArg)==0 ){
useChrootJail = 0;
}
}else
if( strcmp(z, "-debug")==0 ){
if( atoi(zArg) ){
useTimeout = 0;
}
}else
if( strcmp(z, "-input")==0 ){
if( freopen(zArg, "rb", stdin)==0 || stdin==0 ){
Malfunction(514, /* LOG: cannot open --input file */
"cannot open --input file \"%s\"\n", zArg);
}
}else
if( strcmp(z, "-version")==0 ){
puts(SERVER_SOFTWARE_TLS);
return 0;
}else
if( strcmp(z, "-datetest")==0 ){
TestParseRfc822Date();
printf("Ok\n");
exit(0);
}else
if( strcmp(z,"-remote-addr")==0 ){
/* Used for testing purposes only - to simulate a remote IP address when
** input is really coming from a disk file. */
zRemoteAddr = StrDup(zArg);
}else
{
Malfunction(515, /* LOG: unknown command-line argument on launch */
"unknown argument: [%s]\n", z);
}
argv += nTerm;
argc -= nTerm;
}
if( zRoot==0 ){
if( !standalone ){
mnPort = 8080;
mxPort = 8100;
}
standalone = 1;
bLocalhost = 1;
zRoot = ".";
}
/* 10 seconds to get started */
if( useTimeout ){//设置超时信号处理
signal(SIGALRM, Timeout);
signal(SIGSEGV, Timeout);
signal(SIGPIPE, Timeout);
signal(SIGXCPU, Timeout);
if( !standalone ) SetTimeout(10, 806); /* LOG: Timeout startup */
}
#if ENABLE_TLS
/* We "need" to read the cert before chroot'ing to allow that the
** cert is stored in space outside of the --root and not readable by the --user.
*/
if( useHttps>=2 ){
ssl_init_server(tlsState.zCertFile, tlsState.zKeyFile);//初始化SSL
}
#endif
/* Change directories to the root of the HTTP filesystem. Then
** create a chroot jail there. */
if( chdir(zRoot)!=0 ){、、切换到HTTP文件系统的根目录
Malfunction(517, /* LOG: chdir() failed */
"cannot change to directory [%s]", zRoot);
}
/* Get information about the user if available 获得用户信息*/
if( zPermUser ) pwd = getpwnam(zPermUser);
else if( getuid()==0 ){
Malfunction(518, "Cannot run as root. Use the -user USER flag.");
return 1;
}
/* Enter the chroot jail if requested 如果请求了使用 chroot 环境,则进入 chroot 环境*/
if( zPermUser && useChrootJail && getuid()==0 ){
if( chroot(".")<0 ){//尝试将当前目录设为 chroot 环境
Malfunction(519, /* LOG: chroot() failed */
"unable to create chroot jail");
}else{
zRoot = "";
}
}
/* Activate the server, if requested 如果请求了启动服务器,并且端口范围有效,则激活服务器*/
if( mnPort>0 && mnPort<=mxPort
&& http_server(mnPort, mxPort, bLocalhost, zPage, &httpConnection)
){
Malfunction(520, /* LOG: server startup failed */
"failed to start server");
}
#ifdef RLIMIT_CPU//对进程的CPU时间限制。
if( maxCpu>0 ){
struct rlimit rlim;
rlim.rlim_cur = maxCpu;//所需的最大CPU时间。
rlim.rlim_max = maxCpu;
setrlimit(RLIMIT_CPU, &rlim);//调用 setrlimit 函数设置CPU时间限制
}
#endif
/* Drop root privileges.降低 root 权限。 */
if( zPermUser ){
if( pwd ){//如果指定了要切换的用户
if( setgid(pwd->pw_gid) ){//如果获取到了指定用户的信息,则尝试切换组ID和用户ID
Malfunction(521, /* LOG: setgid() failed */
"cannot set group-id to %d", pwd->pw_gid);//切换组ID失败
}
if( setuid(pwd->pw_uid) ){
Malfunction(522, /* LOG: setuid() failed */
"cannot set user-id to %d", pwd->pw_uid);//切换用户ID失败
}
}else{
Malfunction(523, /* LOG: unknown user */
"no such user [%s]", zPermUser);//未找到指定的用户
}
}
if( getuid()==0 ){
Malfunction(524, /* LOG: cannot run as root */
"cannot run as root");
}
/* Get the IP address from whence the request originates
获取请求的来源IP地址*/
if( zRemoteAddr==0 ){//如果未指定远程地址,则尝试获取连接的对端IP地址
address remoteAddr;
unsigned int size = sizeof(remoteAddr);
char zHost[NI_MAXHOST];
if( getpeername(0, &remoteAddr.sa, &size)>=0 ){
//如果获取成功,则使用 getnameinfo 函数将对端IP地址转换为字符串形式
getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0,
NI_NUMERICHOST);
zRemoteAddr = StrDup(zHost);
}
}
//对IPv6地址进行处理,去除前缀 '::ffff:' 并检查是否包含IPv4地址
if( zRemoteAddr!=0
&& strncmp(zRemoteAddr, "::ffff:", 7)==0
&& strchr(zRemoteAddr+7, ':')==0
&& strchr(zRemoteAddr+7, '.')!=0
){
zRemoteAddr += 7;//如果是IPv6转换的IPv4地址,则去除前缀 '::ffff:'
}
//根据是否使用HTTPS设置服务器软件信息
zServerSoftware = useHttps==2 ? SERVER_SOFTWARE_TLS : SERVER_SOFTWARE;
/* Process the input stream 处理输入流*/
for(i=0; i<100; i++){
ProcessOneRequest(0, httpConnection);
}
ProcessOneRequest(1, httpConnection);// 处理最后一个请求
tls_close_conn();//并关闭TLS连接
exit(0);
}