mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 12:55:49 +08:00
http server implemented, pcre support for hostnames in acls implemented
This commit is contained in:
parent
ea4b2cc3a2
commit
9529a1dfcf
@ -34,6 +34,7 @@
|
||||
<li><a href="#ISFTP">How to set up an FTP proxy</a></li>
|
||||
<li><a href="#TLSPR">How to set up an SNI proxy (tlspr)</a></li>
|
||||
<li><a href="#DNSPR">How to set up a DNS proxy (dnspr)</a></li>
|
||||
<li><a href="#HTTPSRV">How to serve pages with the built-in HTTP server (httpsrv)</a></li>
|
||||
<li><a href="#SSLPLUGIN">How to set up TLS/SSL (https proxy, mTLS)</a></li>
|
||||
<li><a href="#CERTIFICATES">How to create CA and certificates for SSL</a></li>
|
||||
<li><a href="#PCRE">How to use PCRE filtering (regular expressions)</a></li>
|
||||
@ -727,6 +728,159 @@ nscache 65536
|
||||
nscache6 65536
|
||||
dnspr -p53 -F10.0.0.1
|
||||
</pre>
|
||||
</p>
|
||||
<li><a name="HTTPSRV"><i>How to serve pages with the built-in HTTP server (httpsrv)</i></a>
|
||||
<p>
|
||||
httpsrv answers requests itself instead of forwarding them. What it does with a
|
||||
request is decided by <code>http</code> rules written before the service, the way
|
||||
access rules are: the first rule whose host and URL both match handles the
|
||||
request. It is useful for a status page, a small static site, a block page for
|
||||
requests an ACL rejects, or a health check an upstream balancer can poll.
|
||||
</p><pre>
|
||||
http OPERATION HOST URL [PARAMETERS]
|
||||
</pre>
|
||||
<p>
|
||||
HOST is matched against the Host header, URL against the path with the query
|
||||
string removed. A minimal static site:
|
||||
</p><pre>
|
||||
auth iponly
|
||||
allow *
|
||||
|
||||
http file * / /usr/local/web/index.html
|
||||
http file * /*.html "/usr/local/web/$1.html"
|
||||
http file * /css/*.css "/usr/local/web/css/$1.css"
|
||||
http cache * /img/** "/usr/local/web/img/$1" * 3600
|
||||
httpsrv -p80 -i127.0.0.1
|
||||
</pre>
|
||||
<p>
|
||||
<b>Patterns.</b> <code>*</code> stands for any run of characters within one
|
||||
element of the path and does not cross a <code>/</code>, so a rule cannot reach
|
||||
into a directory it did not name. <code>**</code> crosses them. Each star, and
|
||||
each group of a regular expression, is remembered in order: <code>$1</code>
|
||||
upwards stand for them in the path or location the rule builds, and
|
||||
<code>$0</code> for the whole request path. A <code>rewrite_host</code> rule
|
||||
uses the stars of its own host pattern instead, since that is what it is
|
||||
rewriting. With a PCRE build a pattern may be
|
||||
written as a regular expression with a <code>pcre:</code> prefix, for the URL and
|
||||
for the host alike:
|
||||
</p><pre>
|
||||
http file * /d/*.txt "/usr/local/web/$1.txt"
|
||||
http cache * "pcre:^/(.*)/pic/(.*)\.(gif|jpeg)$" "/usr/local/web/picts/$1/$2.$3"
|
||||
http file status.example.com /** "/usr/local/web/status/$1"
|
||||
http file "pcre:^(www|web)\.example\.com$" /** "/usr/local/web/$1"
|
||||
</pre>
|
||||
<p>
|
||||
Outside quotes a dollar begins the name of a file to include, so an argument
|
||||
holding one - a path built with <code>$1</code>, a regular expression anchored
|
||||
with <code>$</code> - is written in quotes, as above. <code>$$</code> stands for
|
||||
a single dollar and is not read as an include either.
|
||||
</p>
|
||||
<p>
|
||||
<b>Operations.</b>
|
||||
</p><pre>
|
||||
# file - send the file, using sendfile/TransmitFile where the system can
|
||||
http file * /dl/** "/usr/local/web/dl/$1"
|
||||
|
||||
# cache - read it into memory on the first request and answer from there
|
||||
http cache * /css/*.css "/usr/local/web/css/$1.css"
|
||||
|
||||
# redir - answer with a redirect, 302 unless a status is given
|
||||
http redir * /old/** 301 "https://example.org/$1"
|
||||
|
||||
# rewrite - change the path and hand the request to the rules after this one
|
||||
http rewrite * /alias/** "/w/$1"
|
||||
|
||||
# rewrite_host - the same for the host, which decides which rules match next
|
||||
http rewrite_host *.old.example ** "$1.new.example"
|
||||
|
||||
# reply - a status and nothing else
|
||||
http reply * /health** 200 "X-Health: ok"
|
||||
http reply * /down** 503 "Retry-After: 30"
|
||||
|
||||
# echo, data - describe the request, or generate content of a given size
|
||||
http echo * /echo**
|
||||
http data * /gen** size=1048576
|
||||
</pre>
|
||||
<p>
|
||||
<b>What a rule adds to the answer.</b> <code>file</code> and <code>cache</code>
|
||||
take, after the path, a content type, a max-age, headers to add and a status to
|
||||
answer with. Each may be left out or written as <code>*</code>:
|
||||
</p><pre>
|
||||
http OPERATION HOST URL PATH [TYPE [MAX-AGE [HEADERS [CODE]]]]
|
||||
|
||||
# type worked out from the name, cached by clients for an hour
|
||||
http file * /img/*.png "/usr/local/web/img/$1.png" * 3600
|
||||
|
||||
# a type of its own, and a header
|
||||
http file * /api/*.json "/usr/local/web/api/$1.json" application/json * "X-Api: 1"
|
||||
|
||||
# a file serving as the body of an error page
|
||||
http file * /err/** /usr/local/web/404.html text/html * * 404
|
||||
</pre>
|
||||
<p>
|
||||
HEADERS is one argument holding whole header lines, separated by a backslash and
|
||||
an n - the two characters, since a configuration line cannot carry a line
|
||||
ending. Quote it, headers contain spaces. A rule's headers and max-age go with
|
||||
whatever status that rule asked for, but not with a refusal the server itself
|
||||
decided on: a request for a file which is not there is answered 404 by the
|
||||
server, not by the rule.
|
||||
</p>
|
||||
<p>
|
||||
Types not known to the server are registered with
|
||||
<code>http_content_type</code>, and a type named by a rule is used whatever the
|
||||
name of the file says:
|
||||
</p><pre>
|
||||
http_content_type .webp image/webp
|
||||
http_content_type wasm application/wasm
|
||||
</pre>
|
||||
<p>
|
||||
<b>Files and dates.</b> Only a full path is taken - a relative one would be read
|
||||
against whatever directory the service happens to be in - and a path holding
|
||||
<code>.</code> or <code>..</code> as an element, a line ending or a star is
|
||||
refused. On Windows a path must name a drive or a share (<code>"C:\web\$1"</code>
|
||||
or <code>"\\host\share\$1"</code>). A request which decodes to a path leaving the
|
||||
tree is refused before any of this. Every answer carries Last-Modified, and a
|
||||
request carrying If-Modified-Since is answered 304 with no body when the file
|
||||
has not changed.
|
||||
</p>
|
||||
<p>
|
||||
<b>Caching.</b> <code>cache</code> reads the file once and answers from memory
|
||||
afterwards; a file which has changed on disk is read again, and one larger than
|
||||
a megabyte is sent as <code>file</code> would. With a MAX-AGE the file is not
|
||||
looked at again for that long - the rule has already told clients the file may
|
||||
be treated as unchanged for that time - so a request costs nothing but the copy
|
||||
out. Without one every request stats the file and a change is picked up at once.
|
||||
</p>
|
||||
<p>
|
||||
<b>A block page.</b> A service which rejects a request with a redirect can send
|
||||
the client to an httpsrv running beside it:
|
||||
</p><pre>
|
||||
auth iponly
|
||||
deny * * "pcre:^(ads|track)[0-9]*\.example\.(com|net)$"
|
||||
allow *
|
||||
proxy -p3128 -i192.168.1.1
|
||||
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http file * /** /usr/local/web/blocked.html text/html * * 403
|
||||
httpsrv -p8080 -i127.0.0.1
|
||||
</pre>
|
||||
<p>
|
||||
<b>Connections.</b> A client asking in HTTP/1.1 gets a 1.1 answer and the
|
||||
connection is kept for the next request, unless it sent
|
||||
<code>Connection: close</code>; a 1.0 client has to ask for keep-alive. The
|
||||
connection is only kept when the length of the answer is known exactly, which is
|
||||
true of every operation except the administration pages, so those are always the
|
||||
last thing on a connection. A request body the server cannot read to its end -
|
||||
one sent chunked, or one larger than a megabyte - ends the connection too.
|
||||
</p>
|
||||
<p>
|
||||
<b>Administration.</b> The <code>admin</code> service is httpsrv with the pages
|
||||
of the administration interface already declared, see
|
||||
<a href="#ADMIN">Administering and information analysis</a>. Rules may be added
|
||||
before it in the same way, and are taken first.
|
||||
</p>
|
||||
</p>
|
||||
<li><a name="SSLPLUGIN"><i>How to set up TLS/SSL (https proxy, mTLS)</i></a>
|
||||
<p>
|
||||
@ -1223,6 +1377,34 @@ pcre_extend deny * 192.168.0.1/16
|
||||
<p>
|
||||
<b>Note:</b> Regular expressions don't require authentication and cannot replace
|
||||
authentication and/or allow/deny ACLs.
|
||||
</p>
|
||||
<p>
|
||||
<b>Regular expressions in host names:</b> a host name in the target list of an
|
||||
access rule may be written as a regular expression instead of a wildmask, by
|
||||
giving it a <code>pcre:</code> prefix (<code>regex:</code> means the same). This
|
||||
needs a build with PCRE support, the same as the <code>pcre</code> commands
|
||||
above.
|
||||
</p><pre>
|
||||
# Wildmask: a name may only be matched at its beginning and its end
|
||||
deny * * *ads.example.com
|
||||
|
||||
# Regular expression: anything PCRE can express
|
||||
deny * * "pcre:^(ads|track)[0-9]*\.example\.(com|net)$"
|
||||
allow * * "pcre:^(www|api)\.example\.com$"
|
||||
</pre>
|
||||
<p>
|
||||
The name is lowercased and trailing dots are removed before the pattern is
|
||||
matched, so write patterns in lower case. Quote a pattern which ends in
|
||||
<code>$</code>, or write it as <code>$$</code>: outside quotes a lone dollar
|
||||
begins the name of a file to include. Only the target list takes names - the
|
||||
source list is addresses - and the name is only checked when the request
|
||||
carries one. A wildmask is cheaper and is enough for most rules; a regular
|
||||
expression is matched per request.
|
||||
</p>
|
||||
<p>
|
||||
The same prefix and the same patterns are used by the <code>http</code> command
|
||||
of the built-in HTTP server, for the host a rule answers for and for the URL it
|
||||
matches.
|
||||
</p>
|
||||
<li><A NAME="AUTH">How to limit service access</a>
|
||||
<p>
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
<li><a href="#ISFTP">Как настроить FTP прокси?</a></li>
|
||||
<li><a href="#TLSPR">Как настроить SNI proxy (tlspr)</a></li>
|
||||
<li><a href="#DNSPR">Как настроить DNS proxy (dnspr)</a></li>
|
||||
<li><a href="#HTTPSRV">Как отдавать страницы встроенным HTTP-сервером (httpsrv)</a></li>
|
||||
<li><a href="#SSLPLUGIN">Как настроить TLS/SSL (https прокси, mTLS)</a></li>
|
||||
<li><a href="#CERTIFICATES">Как создать CA и сертификаты для SSL</a></li>
|
||||
<li><a href="#PCRE">Как использовать PCRE-фильтрацию (регулярные выражения)</a></li>
|
||||
@ -738,6 +739,160 @@ dnspr -p53 -F10.0.0.1
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<li><a name="HTTPSRV"><i>Как отдавать страницы встроенным HTTP-сервером (httpsrv)</i></a>
|
||||
<p>
|
||||
httpsrv отвечает на запросы сам, а не пересылает их. Что делать с запросом,
|
||||
определяют правила <code>http</code>, записанные перед сервисом, как и правила
|
||||
доступа: запрос обрабатывает первое правило, у которого совпали и хост, и URL.
|
||||
Это удобно для страницы состояния, небольшого статического сайта, страницы
|
||||
блокировки для запросов, отклонённых ACL, или health check, который опрашивает
|
||||
вышестоящий балансировщик.
|
||||
</p><pre>
|
||||
http ОПЕРАЦИЯ ХОСТ URL [ПАРАМЕТРЫ]
|
||||
</pre>
|
||||
<p>
|
||||
ХОСТ сопоставляется с заголовком Host, URL - с путём без строки запроса.
|
||||
Минимальный статический сайт:
|
||||
</p><pre>
|
||||
auth iponly
|
||||
allow *
|
||||
|
||||
http file * / /usr/local/web/index.html
|
||||
http file * /*.html "/usr/local/web/$1.html"
|
||||
http file * /css/*.css "/usr/local/web/css/$1.css"
|
||||
http cache * /img/** "/usr/local/web/img/$1" * 3600
|
||||
httpsrv -p80 -i127.0.0.1
|
||||
</pre>
|
||||
<p>
|
||||
<b>Шаблоны.</b> <code>*</code> означает любую последовательность символов внутри
|
||||
одного элемента пути и не пересекает <code>/</code>, поэтому правило не может
|
||||
попасть в каталог, который не назван в нём. <code>**</code> пересекает.
|
||||
Каждая звёздочка и каждая группа регулярного выражения запоминаются по порядку:
|
||||
<code>$1</code> и далее подставляют их в путь или адрес, который строит правило,
|
||||
<code>$0</code> - весь путь запроса. Правило <code>rewrite_host</code>
|
||||
использует звёздочки собственного шаблона хоста, поскольку переписывает именно
|
||||
его. В сборке с PCRE шаблон можно записать
|
||||
регулярным выражением с префиксом <code>pcre:</code> - и для URL, и для хоста:
|
||||
</p><pre>
|
||||
http file * /d/*.txt "/usr/local/web/$1.txt"
|
||||
http cache * "pcre:^/(.*)/pic/(.*)\.(gif|jpeg)$" "/usr/local/web/picts/$1/$2.$3"
|
||||
http file status.example.com /** "/usr/local/web/status/$1"
|
||||
http file "pcre:^(www|web)\.example\.com$" /** "/usr/local/web/$1"
|
||||
</pre>
|
||||
<p>
|
||||
Вне кавычек доллар начинает имя включаемого файла, поэтому аргумент, содержащий
|
||||
доллар - путь с <code>$1</code>, регулярное выражение с якорем <code>$</code> -
|
||||
записывается в кавычках, как выше. <code>$$</code> означает один доллар и тоже
|
||||
не читается как включение файла.
|
||||
</p>
|
||||
<p>
|
||||
<b>Операции.</b>
|
||||
</p><pre>
|
||||
# file - отдать файл, через sendfile/TransmitFile там, где система это умеет
|
||||
http file * /dl/** "/usr/local/web/dl/$1"
|
||||
|
||||
# cache - прочитать в память при первом запросе и отвечать из неё
|
||||
http cache * /css/*.css "/usr/local/web/css/$1.css"
|
||||
|
||||
# redir - ответить редиректом, 302, если код не задан
|
||||
http redir * /old/** 301 "https://example.org/$1"
|
||||
|
||||
# rewrite - изменить путь и передать запрос следующим правилам
|
||||
http rewrite * /alias/** "/w/$1"
|
||||
|
||||
# rewrite_host - то же для хоста, от которого зависит выбор следующих правил
|
||||
http rewrite_host *.old.example ** "$1.new.example"
|
||||
|
||||
# reply - только код ответа, без тела
|
||||
http reply * /health** 200 "X-Health: ok"
|
||||
http reply * /down** 503 "Retry-After: 30"
|
||||
|
||||
# echo, data - описание запроса или генерация содержимого заданного размера
|
||||
http echo * /echo**
|
||||
http data * /gen** size=1048576
|
||||
</pre>
|
||||
<p>
|
||||
<b>Что правило добавляет в ответ.</b> <code>file</code> и <code>cache</code>
|
||||
принимают после пути тип содержимого, max-age, добавляемые заголовки и код
|
||||
ответа. Любой из них можно опустить или записать как <code>*</code>:
|
||||
</p><pre>
|
||||
http ОПЕРАЦИЯ ХОСТ URL ПУТЬ [ТИП [MAX-AGE [ЗАГОЛОВКИ [КОД]]]]
|
||||
|
||||
# тип определяется по имени файла, клиенты кэшируют час
|
||||
http file * /img/*.png "/usr/local/web/img/$1.png" * 3600
|
||||
|
||||
# собственный тип и заголовок
|
||||
http file * /api/*.json "/usr/local/web/api/$1.json" application/json * "X-Api: 1"
|
||||
|
||||
# файл как тело страницы ошибки
|
||||
http file * /err/** /usr/local/web/404.html text/html * * 404
|
||||
</pre>
|
||||
<p>
|
||||
ЗАГОЛОВКИ - один аргумент, содержащий целые строки заголовков, разделённые
|
||||
обратной косой чертой и n - двумя символами, поскольку строка конфигурации не
|
||||
может содержать конец строки. Аргумент нужно брать в кавычки, в заголовках есть
|
||||
пробелы. Заголовки и max-age правила отправляются с тем кодом, который правило
|
||||
задало, но не с отказом, который решил вернуть сам сервер: на запрос
|
||||
отсутствующего файла 404 отвечает сервер, а не правило.
|
||||
</p>
|
||||
<p>
|
||||
Неизвестные серверу типы регистрируются командой
|
||||
<code>http_content_type</code>, а тип, названный в правиле, используется
|
||||
независимо от имени файла:
|
||||
</p><pre>
|
||||
http_content_type .webp image/webp
|
||||
http_content_type wasm application/wasm
|
||||
</pre>
|
||||
<p>
|
||||
<b>Файлы и даты.</b> Принимается только полный путь - относительный отсчитывался
|
||||
бы от того каталога, в котором оказался сервис, - а путь с элементом
|
||||
<code>.</code> или <code>..</code>, концом строки или звёздочкой отвергается. В
|
||||
Windows путь должен указывать диск или сетевой ресурс (<code>"C:\web\$1"</code>
|
||||
или <code>"\\host\share\$1"</code>). Запрос, который декодируется в путь за
|
||||
пределами дерева, отвергается раньше всего этого. Каждый ответ содержит
|
||||
Last-Modified, а запрос с If-Modified-Since получает 304 без тела, если файл не
|
||||
изменился.
|
||||
</p>
|
||||
<p>
|
||||
<b>Кэширование.</b> <code>cache</code> читает файл один раз и дальше отвечает из
|
||||
памяти; изменившийся на диске файл читается заново, а файл больше мегабайта
|
||||
отдаётся так же, как это сделал бы <code>file</code>. При заданном MAX-AGE файл
|
||||
не проверяется в течение этого времени - правило уже сообщило клиентам, что
|
||||
столько файл можно считать неизменным, - и запрос стоит только копирования
|
||||
наружу. Без MAX-AGE каждый запрос делает stat, и изменение подхватывается сразу.
|
||||
</p>
|
||||
<p>
|
||||
<b>Страница блокировки.</b> Сервис, отклоняющий запрос редиректом, может
|
||||
отправить клиента на httpsrv, работающий рядом:
|
||||
</p><pre>
|
||||
auth iponly
|
||||
deny * * "pcre:^(ads|track)[0-9]*\.example\.(com|net)$"
|
||||
allow *
|
||||
proxy -p3128 -i192.168.1.1
|
||||
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http file * /** /usr/local/web/blocked.html text/html * * 403
|
||||
httpsrv -p8080 -i127.0.0.1
|
||||
</pre>
|
||||
<p>
|
||||
<b>Соединения.</b> Клиент, обратившийся по HTTP/1.1, получает ответ 1.1, и
|
||||
соединение сохраняется для следующего запроса, если он не прислал
|
||||
<code>Connection: close</code>; клиенту 1.0 нужно запросить keep-alive явно.
|
||||
Соединение сохраняется только тогда, когда длина ответа известна точно - это
|
||||
верно для всех операций, кроме страниц администрирования, поэтому они всегда
|
||||
последнее, что отдаётся в соединении. Тело запроса, которое сервер не может
|
||||
дочитать до конца - присланное chunked или размером больше мегабайта, - тоже
|
||||
завершает соединение.
|
||||
</p>
|
||||
<p>
|
||||
<b>Администрирование.</b> Сервис <code>admin</code> - это httpsrv с уже
|
||||
объявленными страницами интерфейса администрирования, см.
|
||||
<a href="#ADMIN">Администрирование и анализ информации</a>. Правила можно
|
||||
добавлять перед ним так же, и они проверяются первыми.
|
||||
</p>
|
||||
</p>
|
||||
<li><a name="SSLPLUGIN"><i>Как настроить TLS/SSL (https прокси, mTLS)</i></a>
|
||||
<p>
|
||||
Начиная с версии 0.9.7 поддержка TLS/SSL встроена в 3proxy при компиляции с OpenSSL
|
||||
@ -1221,6 +1376,34 @@ pcre_extend deny * 192.168.0.1/16
|
||||
<p>
|
||||
<b>Примечание:</b> Регулярные выражения не требуют авторизации и не могут заменить
|
||||
авторизацию и/или ACL allow/deny.
|
||||
</p>
|
||||
<p>
|
||||
<b>Регулярные выражения в именах хостов:</b> имя хоста в списке назначения
|
||||
правила доступа может быть записано регулярным выражением вместо маски, для
|
||||
этого используется префикс <code>pcre:</code> (<code>regex:</code> означает то
|
||||
же самое). Требуется сборка с поддержкой PCRE, как и для команд
|
||||
<code>pcre</code> выше.
|
||||
</p><pre>
|
||||
# Маска: имя сопоставляется только с начала и с конца
|
||||
deny * * *ads.example.com
|
||||
|
||||
# Регулярное выражение: всё, что выразимо средствами PCRE
|
||||
deny * * "pcre:^(ads|track)[0-9]*\.example\.(com|net)$"
|
||||
allow * * "pcre:^(www|api)\.example\.com$"
|
||||
</pre>
|
||||
<p>
|
||||
Перед сопоставлением имя приводится к нижнему регистру, завершающие точки
|
||||
удаляются, поэтому шаблоны пишутся в нижнем регистре. Шаблон, оканчивающийся на
|
||||
<code>$</code>, нужно взять в кавычки или записать как <code>$$</code>: вне
|
||||
кавычек одиночный доллар начинает имя включаемого файла. Имена допустимы только
|
||||
в списке назначения (список источника - адреса), и имя проверяется лишь тогда,
|
||||
когда оно присутствует в запросе. Маска обходится дешевле и достаточна для
|
||||
большинства правил, регулярное выражение сопоставляется на каждый запрос.
|
||||
</p>
|
||||
<p>
|
||||
Тот же префикс и те же шаблоны использует команда <code>http</code> встроенного
|
||||
HTTP-сервера - для хоста, на который отвечает правило, и для URL, который оно
|
||||
сопоставляет.
|
||||
</p>
|
||||
|
||||
<li><a name="AUTH"><i>Как ограничить доступ к службе</i></a>
|
||||
|
||||
184
man/3proxy.cfg.5
184
man/3proxy.cfg.5
@ -39,7 +39,9 @@ For included file <CR> (end of line characters) is treated as space character
|
||||
(arguments delimiter instead of end of command delimiter).
|
||||
Thus, include files are only useful to store long single-line commands
|
||||
(like userlist, network lists, etc).
|
||||
To use dollar sign somewhere in argument it must be quoted.
|
||||
To use dollar sign somewhere in argument it must be quoted or doubled: inside
|
||||
quotes a dollar is ordinary text, and \fB$$\fR stands for a single dollar and is
|
||||
not read as an include.
|
||||
Recursion is not allowed.
|
||||
|
||||
.br
|
||||
@ -743,6 +745,17 @@ Since 0.6, the targetlist may also contain host names,
|
||||
instead of addresses. It\'s possible to use a wildmask in
|
||||
the beginning and at the end of the hostname, e.g. *badsite.com or *badcontent*.
|
||||
The hostname is only checked if a hostname is present in the request.
|
||||
A name written with a \fBpcre:\fR prefix (\fBregex:\fR is the same thing) is a
|
||||
regular expression instead of a wildmask, in a build with PCRE support:
|
||||
.br
|
||||
deny * * "pcre:^(ads|track)[0-9]*\\.example\\.(com|net)$"
|
||||
.br
|
||||
The name is lowercased and any trailing dots are removed before it is matched,
|
||||
so patterns are written in lower case. A pattern ending in \fB$\fR has to be
|
||||
quoted or written \fB$$\fR, since a lone dollar outside quotes begins the name
|
||||
of a file to include. The same patterns, and the same prefix, are used by the
|
||||
\fBhttp\fR command, see BUILT IN HTTP SERVER. Regular expressions are matched
|
||||
per request and cost more than a wildmask, which is enough for most rules.
|
||||
Targetportlist may contain ports (X) or port ranges lists (X-Y). For any field *
|
||||
sign means ANY. If access list is empty it\'s assumed to be
|
||||
.br
|
||||
@ -1511,6 +1524,15 @@ PCRE_NOTEMPTY, PCRE_UTF8, PCRE_NO_AUTO_CAPTURE, PCRE_NO_UTF8_CHECK, PCRE_AUTO_CA
|
||||
PCRE_PARTIAL, PCRE_DFA_SHORTEST, PCRE_DFA_RESTART, PCRE_FIRSTLINE, PCRE_DUPNAMES,
|
||||
PCRE_NEWLINE_CR, PCRE_NEWLINE_LF, PCRE_NEWLINE_CRLF, PCRE_NEWLINE_ANY, PCRE_NEWLINE_ANYCRLF,
|
||||
PCRE_BSR_ANYCRLF, PCRE_BSR_UNICODE.
|
||||
.br
|
||||
These options apply to every pattern the configuration compiles, the host
|
||||
patterns of access rules and \fBhttp\fR rules included, so set them before the
|
||||
rules which are to use them.
|
||||
.br
|
||||
Regular expressions are not only for these commands: a host name in the target
|
||||
list of an access rule, and the host and URL of an \fBhttp\fR rule, take one
|
||||
when it is written with a \fBpcre:\fR prefix. See \fBallow\fR and BUILT IN
|
||||
HTTP SERVER.
|
||||
|
||||
.SS PCRE Parameters
|
||||
TYPE - type of filtered data (comma-delimited list):
|
||||
@ -1551,6 +1573,166 @@ matched if the ACL matches the connection data.
|
||||
Warning: Regular expressions don't require authentication and cannot replace
|
||||
authentication and/or allow/deny ACLs.
|
||||
|
||||
.SH BUILT IN HTTP SERVER
|
||||
The \fBhttpsrv\fR service answers requests itself instead of forwarding them.
|
||||
What it does with a request is decided by \fBhttp\fR rules, which are taken in
|
||||
the order they are written: the first whose host and URL both match handles the
|
||||
request. Rules belong to the service that follows them, the way access rules do,
|
||||
and \fBadmin\fR is \fBhttpsrv\fR with a set of rules already in place.
|
||||
|
||||
.BR http
|
||||
\fIOPERATION HOST URL [PARAMETERS]\fR
|
||||
.br
|
||||
Handle a request for \fIURL\fR on \fIHOST\fR with \fIOPERATION\fR. HOST is
|
||||
matched against the Host header, URL against the path, with the query string
|
||||
removed.
|
||||
|
||||
.SS Operations
|
||||
.br
|
||||
\fBfile\fR \fIPATH [TYPE [MAX-AGE [HEADERS [CODE]]]]\fR - send the file at PATH.
|
||||
The file is handed to the socket by the system where it can do that (sendfile,
|
||||
TransmitFile) and read here where it cannot, as when the connection carries TLS.
|
||||
The arguments after PATH are described below, and each of them may be written as
|
||||
\fB*\fR to leave it out.
|
||||
.br
|
||||
\fBcache\fR \fIPATH [TYPE [MAX-AGE [HEADERS [CODE]]]]\fR - the same, but the
|
||||
file is read into memory on the first request and answered from there afterwards.
|
||||
A file that has changed on disk is read again, and one larger than a megabyte is
|
||||
sent as \fBfile\fR would. With a MAX-AGE the file is not looked at again for
|
||||
that long: the rule has already told clients the file may be treated as
|
||||
unchanged for that time, so the server treats its own copy the same way and a
|
||||
request costs nothing but the copy out. Without one every request stats the
|
||||
file, so a change is picked up at once.
|
||||
.br
|
||||
\fBreply\fR \fI[CODE [HEADERS]]\fR - answer with a status and nothing else.
|
||||
CODE is the status to send, 200 without one. A status which carries no body of
|
||||
its own (1xx, 204, 304) is sent without a length; anything else is sent with a
|
||||
length of zero.
|
||||
.br
|
||||
\fBredir\fR \fI[CODE] LOCATION\fR - answer with a redirect. CODE is 301 or 302,
|
||||
or any status from 300 to 399; without one, 302 is used.
|
||||
.br
|
||||
\fBrewrite\fR \fIPATH\fR - change the path of the request and hand it to the
|
||||
rules that follow this one.
|
||||
.br
|
||||
\fBrewrite_host\fR \fIHOST\fR - the same for the host, which decides which of
|
||||
the rules after it match. \fB$1\fR upwards stand for what the stars, or the
|
||||
groups, of this rule\'s host pattern matched, the way they stand for those of
|
||||
the URL in a \fBrewrite\fR. What is built has to be a host name; the name the
|
||||
client sent is what access rules matched and what the log records.
|
||||
.br
|
||||
\fBecho\fR - answer with a description of the request: the method, path, query,
|
||||
host, and the address and port it came from. For testing.
|
||||
.br
|
||||
\fBdata\fR \fI[size=N] [block=N] [status=N] [chunked=1] [delay=N]\fR - answer
|
||||
with generated content of the size asked for. For testing.
|
||||
.br
|
||||
\fBadmin\fR, \fBadmin_counters\fR, \fBadmin_reload\fR, \fBadmin_services\fR -
|
||||
the pages of the administration interface.
|
||||
|
||||
.SS What a rule adds to the answer
|
||||
\fBTYPE\fR is the content type to answer with. Without it, or with \fB*\fR, the
|
||||
type is worked out from the name of the file, see \fBhttp_content_type\fR.
|
||||
.br
|
||||
\fBMAX-AGE\fR is a number of seconds, and is sent as Cache-Control: max-age.
|
||||
Without it, or with \fB*\fR, nothing is said about caching.
|
||||
.br
|
||||
\fBHEADERS\fR is one argument holding whole header lines, separated by a
|
||||
backslash and an n \- the two characters, since a configuration line cannot
|
||||
carry a line ending. Each becomes a real line ending in the answer. Quote the
|
||||
argument if any header holds a space, which they usually do.
|
||||
.br
|
||||
\fBCODE\fR is the status to answer with instead of 200, which is how a file
|
||||
serves as the body of an error page.
|
||||
.br
|
||||
A rule's headers and MAX-AGE go with whatever status that rule asked for. They
|
||||
are not sent with a refusal the server itself decided on: a request for a file
|
||||
which is not there is answered 404 by the server, not by the rule.
|
||||
.br
|
||||
\fBfile\fR and \fBcache\fR send Last-Modified, and answer a request carrying
|
||||
If-Modified-Since with 304 and no body when the file has not changed since the
|
||||
time it names. All three date formats HTTP allows are read; one which cannot be
|
||||
read is treated as no date at all. A rule answering with a CODE of its own is
|
||||
answering something other than the file, so it is never turned into a 304.
|
||||
.br
|
||||
http file * /err/** "/usr/local/web/404.html" text/html * "X-Served: static" 404
|
||||
.br
|
||||
http reply * /health** 200 "X-Health: ok"
|
||||
.br
|
||||
http reply * /down** 503 "Retry-After: 30"
|
||||
|
||||
.BR http_content_type
|
||||
\fIEXTENSION TYPE\fR
|
||||
.br
|
||||
Answer for a file with that extension with that content type, in addition to
|
||||
the types already known. The extension may be written with or without its dot.
|
||||
A type named by a rule is used whatever this says, and a name the server knows
|
||||
nothing about is answered as application/octet-stream.
|
||||
.br
|
||||
http_content_type .webp image/webp
|
||||
|
||||
.SS Patterns
|
||||
A URL is matched with stars, or with a regular expression when it carries a
|
||||
\fBpcre:\fR prefix (\fBregex:\fR is taken as well). A host is matched the way an
|
||||
access list matches one, and takes the same prefix.
|
||||
.br
|
||||
\fB*\fR stands for any run of characters within one element of the path: it
|
||||
does not cross a \fB/\fR, so a rule cannot reach into a directory it did not
|
||||
name.
|
||||
.br
|
||||
\fB**\fR crosses them, and is what a rule which should match everything below a
|
||||
point, or everything at all, is written with.
|
||||
.br
|
||||
Each star, and each group of a regular expression, is remembered in the order
|
||||
it appears. \fB$1\fR upwards stand for them in the path or location a rule
|
||||
builds, and \fB$0\fR for the whole request path.
|
||||
.br
|
||||
Outside quotes a dollar begins the name of a file to include, so an argument
|
||||
holding one \- a path or location built with \fB$1\fR, a regular expression
|
||||
anchored with \fB$\fR \- is written in quotes. \fB$$\fR stands for a single
|
||||
dollar and is not read as an include either, which is how a dollar reaches a
|
||||
rule as text.
|
||||
|
||||
.SS Connections
|
||||
An answer is sent as HTTP/1.1 to a client which asked in HTTP/1.1, and the
|
||||
connection is kept for the next request unless the client sent
|
||||
\fBConnection: close\fR. A 1.0 client gets a 1.0 answer, and the connection is
|
||||
kept only when it asked with \fBConnection: keep-alive\fR.
|
||||
.br
|
||||
The connection is kept only when what was sent is framed exactly: every
|
||||
operation but the administration pages states a length, or sends a chunked body
|
||||
a 1.1 client can read, so the pages of \fBadmin\fR are always the last thing on
|
||||
a connection. A request body which cannot be read to its end ends the connection
|
||||
as well: one sent with \fBTransfer-Encoding\fR, which this server does not read,
|
||||
and one longer than a megabyte, which it will not.
|
||||
|
||||
.SS Paths a rule builds
|
||||
The path a rule builds is used as it is, so it is refused rather than corrected
|
||||
when it is not a plain full path. A relative path is refused: it would be read
|
||||
against whatever directory the service happens to be in. So is one holding
|
||||
\fB.\fR or \fB..\fR as an element, a carriage return, a newline or a star. On
|
||||
Windows a path must name a drive or a share, and is converted to the extended
|
||||
\\\\?\\ form and opened through the wide interface, so a long path works.
|
||||
.br
|
||||
A request is checked before any of this: a path which decodes to one leaving
|
||||
the tree is refused outright.
|
||||
|
||||
.SS Examples
|
||||
.br
|
||||
http file example.com /my/webpath/*.html "/usr/local/web/$1.html"
|
||||
.br
|
||||
http cache example.com "pcre:^/(.*)/pic/(.*)\.(gif|jpeg)$" "/usr/local/web/picts/$1/$2.$3"
|
||||
.br
|
||||
http redir * /old/** 301 "https://example.org/$1"
|
||||
.br
|
||||
http rewrite * /alias/** "/w/$1"
|
||||
.br
|
||||
http rewrite_host *.old.example ** "$1.new.example"
|
||||
.br
|
||||
http file * /static/** "/usr/local/web/static/$1"
|
||||
.br
|
||||
httpsrv -p8080
|
||||
|
||||
.SH BUGS
|
||||
Report all bugs to
|
||||
.BR 3proxy@3proxy.org
|
||||
|
||||
@ -16,6 +16,9 @@ void pcre_install(void);
|
||||
#ifdef WITH_TRANSPARENT
|
||||
void transparent_install(void);
|
||||
#endif
|
||||
#ifdef WITH_HTTPSRV
|
||||
void httpsrv_init(void);
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#ifndef NOPLUGINS
|
||||
@ -535,6 +538,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
|
||||
#ifdef WITH_TRANSPARENT
|
||||
transparent_install();
|
||||
#endif
|
||||
#ifdef WITH_HTTPSRV
|
||||
httpsrv_init();
|
||||
#endif
|
||||
|
||||
freeconf(&conf);
|
||||
initcommands();
|
||||
|
||||
142
src/common.c
142
src/common.c
@ -817,11 +817,43 @@ int bindwithrange(struct clientparam *param, SOCKET sock, PROXYSOCKADDRTYPE *sa,
|
||||
whatever replaces the star with a regular expression later - behaves the same
|
||||
way and gains the same syntax at the same time.
|
||||
*/
|
||||
/* A pattern written as a regular expression carries a prefix. Both spellings
|
||||
are taken so a configuration reads the way its author thinks of it. */
|
||||
static unsigned char * regexprefix(unsigned char *arg)
|
||||
{
|
||||
if(!strncmp((char *)arg, "pcre:", 5)) return arg + 5;
|
||||
if(!strncmp((char *)arg, "regex:", 6)) return arg + 6;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Shared by every pattern the configuration can carry. Returns 0 on success. */
|
||||
static int compileregex(struct hostname *h, unsigned char *pattern)
|
||||
{
|
||||
#ifdef WITH_PCRE
|
||||
char err[256];
|
||||
|
||||
h->re = pcre_pattern_compile(pattern, err, sizeof(err));
|
||||
if(!h->re){
|
||||
fprintf(stderr, "Bad regular expression '%s': %s\n", pattern, err);
|
||||
return 1;
|
||||
}
|
||||
h->matchtype = MATCHREGEX;
|
||||
h->name = (unsigned char *)strdup((char *)pattern);
|
||||
return h->name? 0 : 1;
|
||||
#else
|
||||
fprintf(stderr, "Regular expression '%s' needs a build with PCRE\n", pattern);
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int parsepattern(struct hostname *h, unsigned char *arg)
|
||||
{
|
||||
int arglen;
|
||||
unsigned char *pattern;
|
||||
|
||||
h->re = NULL;
|
||||
if((pattern = regexprefix(arg))) return compileregex(h, pattern);
|
||||
|
||||
arglen = (int)strlen((char *)arg);
|
||||
h->matchtype = 3;
|
||||
pattern = arg;
|
||||
@ -851,6 +883,14 @@ int patternmatchpos(const struct hostname *h, const unsigned char *str, int *sta
|
||||
char *found;
|
||||
|
||||
if(!h->name || !str) return 0;
|
||||
if(h->matchtype == MATCHREGEX || h->matchtype == MATCHGLOB){
|
||||
struct capture caps[MAXCAPTURES];
|
||||
|
||||
if(!patternmatchcaps(h, str, caps, NULL)) return 0;
|
||||
if(start) *start = caps[1].start;
|
||||
if(len) *len = caps[1].len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
lname = (int)strlen((char *)h->name);
|
||||
lstr = (int)strlen((char *)str);
|
||||
@ -903,9 +943,111 @@ int patternmatchpos(const struct hostname *h, const unsigned char *str, int *sta
|
||||
|
||||
int patternmatch(const struct hostname *h, const unsigned char *str)
|
||||
{
|
||||
return patternmatchcaps(h, str, NULL, NULL);
|
||||
}
|
||||
|
||||
/* Match a glob, recording what each star stood for.
|
||||
|
||||
A single star stands for any run of characters within one element of the
|
||||
path, so it stops at a slash; a double star crosses them. Stars are
|
||||
numbered in the order they appear, which is how a template refers to them.
|
||||
*/
|
||||
static int globmatch(const unsigned char *pat, const unsigned char *str,
|
||||
const unsigned char *subject, struct capture *caps, int maxcaps, int star)
|
||||
{
|
||||
while(*pat){
|
||||
if(*pat == '*'){
|
||||
int crosses = (pat[1] == '*');
|
||||
const unsigned char *rest = pat + (crosses? 2 : 1);
|
||||
int len;
|
||||
|
||||
for(len = 0; ; len++){
|
||||
if(star < maxcaps && caps){
|
||||
caps[star].start = (int)(str - subject);
|
||||
caps[star].len = len;
|
||||
}
|
||||
if(globmatch(rest, str + len, subject, caps, maxcaps, star + 1)) return 1;
|
||||
if(!str[len]) return 0;
|
||||
if(!crosses && str[len] == '/') return 0;
|
||||
}
|
||||
}
|
||||
if(*pat != *str) return 0;
|
||||
pat++;
|
||||
str++;
|
||||
}
|
||||
return *str == 0;
|
||||
}
|
||||
|
||||
/* Match a pattern of any kind and report what its stars or groups stood for.
|
||||
caps may be NULL when only the yes or no answer is wanted. */
|
||||
int patternmatchcaps(const struct hostname *h, const unsigned char *str,
|
||||
struct capture *caps, int *ncaps)
|
||||
{
|
||||
int n = 0;
|
||||
|
||||
if(ncaps) *ncaps = 0;
|
||||
if(!h || !str) return 0;
|
||||
|
||||
if(h->matchtype == MATCHREGEX){
|
||||
#ifdef WITH_PCRE
|
||||
struct capture local[MAXCAPTURES];
|
||||
|
||||
n = pcre_pattern_match(h->re, str, caps? caps : local, MAXCAPTURES);
|
||||
if(ncaps) *ncaps = n;
|
||||
return n > 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
if(h->matchtype == MATCHGLOB){
|
||||
struct capture local[MAXCAPTURES];
|
||||
struct capture *use = caps? caps : local;
|
||||
int i;
|
||||
|
||||
for(i = 0; i < MAXCAPTURES; i++){
|
||||
use[i].start = 0;
|
||||
use[i].len = 0;
|
||||
}
|
||||
use[0].start = 0;
|
||||
use[0].len = (int)strlen((char *)str);
|
||||
if(!h->name) return 0;
|
||||
if(!globmatch(h->name, str, str, use, MAXCAPTURES, 1)) return 0;
|
||||
if(ncaps){
|
||||
for(n = MAXCAPTURES - 1; n > 0 && !use[n].len && !use[n].start; n--);
|
||||
*ncaps = n + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* the star at one end or both, as an access rule has always written it */
|
||||
if(caps){
|
||||
int start = 0, len = 0;
|
||||
|
||||
if(!patternmatchpos(h, str, &start, &len)) return 0;
|
||||
caps[0].start = 0;
|
||||
caps[0].len = (int)strlen((char *)str);
|
||||
caps[1].start = start;
|
||||
caps[1].len = len;
|
||||
if(ncaps) *ncaps = 2;
|
||||
return 1;
|
||||
}
|
||||
return patternmatchpos(h, str, NULL, NULL);
|
||||
}
|
||||
|
||||
/* A URL in an http rule: stars anywhere, or a regular expression. */
|
||||
int parsepathpattern(struct hostname *h, unsigned char *arg)
|
||||
{
|
||||
unsigned char *pattern;
|
||||
|
||||
h->re = NULL;
|
||||
if((pattern = regexprefix(arg))) return compileregex(h, pattern);
|
||||
|
||||
h->matchtype = MATCHGLOB;
|
||||
h->name = (unsigned char *)strdup((char *)arg);
|
||||
return h->name? 0 : 1;
|
||||
}
|
||||
|
||||
int scanaddr(const unsigned char *s, uint32_t * ip, uint32_t * mask) {
|
||||
unsigned d1, d2, d3, d4, m;
|
||||
int res;
|
||||
|
||||
160
src/conf.c
160
src/conf.c
@ -9,7 +9,7 @@
|
||||
#include "proxy.h"
|
||||
|
||||
#ifdef WITH_HTTPSRV
|
||||
static int addhttprule(char *host, char *url, char *op, char *params);
|
||||
static int addhttprule(char *op, char *host, char *url, char *params);
|
||||
#endif
|
||||
#include "mdhash.h"
|
||||
#ifdef WITH_SSL
|
||||
@ -264,10 +264,10 @@ static int h_proxy(int argc, unsigned char ** argv){
|
||||
else if(!strcmp((char *)argv[0], "admin")) {
|
||||
/* The same service as httpsrv, with the administration pages
|
||||
declared for it. */
|
||||
if(addhttprule("*", "/C*", "admin_counters", NULL) ||
|
||||
addhttprule("*", "/R", "admin_reload", NULL) ||
|
||||
addhttprule("*", "/S*", "admin_services", NULL) ||
|
||||
addhttprule("*", "*", "admin", NULL)){
|
||||
if(addhttprule("admin_counters", "*", "/C*", NULL) ||
|
||||
addhttprule("admin_reload", "*", "/R", NULL) ||
|
||||
addhttprule("admin_services", "*", "/S*", NULL) ||
|
||||
addhttprule("admin", "*", "**", NULL)){
|
||||
fprintf(stderr, "Failed to declare the admin pages, line %d\n", linenum);
|
||||
return 1;
|
||||
}
|
||||
@ -802,8 +802,57 @@ struct redirdesc redirs[] = {
|
||||
};
|
||||
|
||||
#ifdef WITH_HTTPSRV
|
||||
/* Headers a rule adds are written as one argument, the lines separated by a
|
||||
backslash and an n, because a configuration line cannot hold a line ending.
|
||||
Those two characters become a real one here. A line ending which reached the
|
||||
argument as itself is dropped: what goes on the wire is decided here and not
|
||||
by whatever produced the string. */
|
||||
static unsigned char * parsehdrs(const unsigned char *arg)
|
||||
{
|
||||
unsigned char *out, *o;
|
||||
const unsigned char *p;
|
||||
size_t len = strlen((char *)arg);
|
||||
|
||||
out = malloc(len * 2 + 3);
|
||||
if(!out) return NULL;
|
||||
for(p = arg, o = out; *p; p++){
|
||||
if(*p == '\\' && p[1] == 'n'){
|
||||
*o++ = '\r';
|
||||
*o++ = '\n';
|
||||
p++;
|
||||
continue;
|
||||
}
|
||||
if(*p == '\r' || *p == '\n') continue;
|
||||
*o++ = *p;
|
||||
}
|
||||
if(o == out || o[-1] != '\n'){
|
||||
*o++ = '\r';
|
||||
*o++ = '\n';
|
||||
}
|
||||
*o = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* An optional argument which a star, or nothing at all, leaves at its
|
||||
default. */
|
||||
static int optnum(int argc, unsigned char **argv, int at, int def)
|
||||
{
|
||||
if(argc <= at || !strcmp((char *)argv[at], "*")) return def;
|
||||
return atoi((char *)argv[at]);
|
||||
}
|
||||
|
||||
static void freehttprule(struct httprule *rule)
|
||||
{
|
||||
if(rule->host.name) free(rule->host.name);
|
||||
if(rule->url.name) free(rule->url.name);
|
||||
if(rule->params) free(rule->params);
|
||||
if(rule->ctype) free(rule->ctype);
|
||||
if(rule->hdrs) free(rule->hdrs);
|
||||
free(rule);
|
||||
}
|
||||
|
||||
/* Installs one rule from code, for the pages a service predefines. */
|
||||
static int addhttprule(char *host, char *url, char *op, char *params)
|
||||
static int addhttprule(char *op, char *host, char *url, char *params)
|
||||
{
|
||||
struct httprule *rule, *tail;
|
||||
unsigned char hostbuf[64], urlbuf[128];
|
||||
@ -811,6 +860,7 @@ static int addhttprule(char *host, char *url, char *op, char *params)
|
||||
rule = malloc(sizeof(struct httprule));
|
||||
if(!rule) return 1;
|
||||
memset(rule, 0, sizeof(struct httprule));
|
||||
rule->maxage = -1;
|
||||
|
||||
rule->op = httpopbyname((unsigned char *)op);
|
||||
if(rule->op < 0){
|
||||
@ -820,7 +870,7 @@ static int addhttprule(char *host, char *url, char *op, char *params)
|
||||
|
||||
strcpy((char *)hostbuf, host);
|
||||
strcpy((char *)urlbuf, url);
|
||||
if(parsepattern(&rule->host, hostbuf) || parsepattern(&rule->url, urlbuf)){
|
||||
if(parsepattern(&rule->host, hostbuf) || parsepathpattern(&rule->url, urlbuf)){
|
||||
free(rule->host.name);
|
||||
free(rule);
|
||||
return 1;
|
||||
@ -973,9 +1023,10 @@ static int h_http(int argc, unsigned char **argv){
|
||||
struct httprule *rule, *tail;
|
||||
int op;
|
||||
|
||||
op = httpopbyname(argv[3]);
|
||||
/* http OPERATION HOST URL [PARAMETERS] */
|
||||
op = httpopbyname(argv[1]);
|
||||
if(op < 0){
|
||||
fprintf(stderr, "Unknown http operation: %s line %d\n", argv[3], linenum);
|
||||
fprintf(stderr, "Unknown http operation: %s line %d\n", argv[1], linenum);
|
||||
return(1);
|
||||
}
|
||||
|
||||
@ -983,23 +1034,75 @@ static int h_http(int argc, unsigned char **argv){
|
||||
if(!rule) return(21);
|
||||
memset(rule, 0, sizeof(struct httprule));
|
||||
rule->op = op;
|
||||
rule->maxage = -1;
|
||||
|
||||
if(parsepattern(&rule->host, argv[1]) || parsepattern(&rule->url, argv[2])){
|
||||
if(parsepattern(&rule->host, argv[2]) || parsepathpattern(&rule->url, argv[3])){
|
||||
fprintf(stderr, "No memory for http rule, line %d\n", linenum);
|
||||
free(rule->host.name);
|
||||
free(rule);
|
||||
return(21);
|
||||
}
|
||||
|
||||
if(argc > 4){
|
||||
if(argc > 4 && (!strcmp((char *)argv[1], "file") || !strcmp((char *)argv[1], "cache"))){
|
||||
/* PATH [TYPE [MAX-AGE [HEADERS [CODE]]]]. A star, or nothing,
|
||||
leaves each of them out: the type is worked out from the name,
|
||||
nothing is said about caching, no headers are added and the
|
||||
answer is the usual 200. */
|
||||
rule->params = (unsigned char *)strdup((char *)argv[4]);
|
||||
if(!rule->params){
|
||||
free(rule->host.name);
|
||||
free(rule->url.name);
|
||||
free(rule);
|
||||
if(argc > 5 && strcmp((char *)argv[5], "*"))
|
||||
rule->ctype = (unsigned char *)strdup((char *)argv[5]);
|
||||
rule->maxage = optnum(argc, argv, 6, -1);
|
||||
if(argc > 7 && strcmp((char *)argv[7], "*"))
|
||||
rule->hdrs = parsehdrs(argv[7]);
|
||||
rule->code = optnum(argc, argv, 8, 0);
|
||||
if(!rule->params
|
||||
|| (argc > 5 && strcmp((char *)argv[5], "*") && !rule->ctype)
|
||||
|| (argc > 7 && strcmp((char *)argv[7], "*") && !rule->hdrs)){
|
||||
freehttprule(rule);
|
||||
return(21);
|
||||
}
|
||||
}
|
||||
else if(!strcmp((char *)argv[1], "reply")){
|
||||
/* CODE [HEADERS], and no body at all. */
|
||||
rule->code = optnum(argc, argv, 4, 200);
|
||||
if(argc > 5 && strcmp((char *)argv[5], "*")){
|
||||
rule->hdrs = parsehdrs(argv[5]);
|
||||
if(!rule->hdrs){
|
||||
freehttprule(rule);
|
||||
return(21);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(argc > 4){
|
||||
/* What follows the URL belongs to the operation, and an operation
|
||||
such as redir reads more than one word of it. */
|
||||
int i, len = 0;
|
||||
|
||||
for(i = 4; i < argc; i++) len += (int)strlen((char *)argv[i]) + 1;
|
||||
rule->params = malloc(len);
|
||||
if(rule->params){
|
||||
int at = 0;
|
||||
|
||||
for(i = 4; i < argc; i++)
|
||||
at += sprintf((char *)rule->params + at, "%s%s",
|
||||
i > 4? " " : "", argv[i]);
|
||||
}
|
||||
if(!rule->params){
|
||||
freehttprule(rule);
|
||||
return(21);
|
||||
}
|
||||
}
|
||||
|
||||
if(rule->code && (rule->code < 100 || rule->code > 599)){
|
||||
fprintf(stderr, "Wrong http status: %d line %d\n", rule->code, linenum);
|
||||
freehttprule(rule);
|
||||
return(1);
|
||||
}
|
||||
if(rule->maxage < -1){
|
||||
fprintf(stderr, "Wrong max-age, line %d\n", linenum);
|
||||
freehttprule(rule);
|
||||
return(1);
|
||||
}
|
||||
|
||||
if(!conf.httprules) conf.httprules = rule;
|
||||
else {
|
||||
@ -1495,7 +1598,7 @@ static int h_ace(int argc, unsigned char **argv){
|
||||
tl->ace = acl;
|
||||
|
||||
if((acl->action == COUNTIN)||(acl->action == COUNTOUT)||(acl->action == COUNTALL)) {
|
||||
unsigned long lim;
|
||||
uint64_t lim = 0;
|
||||
|
||||
tl->comment = ( char *)argv[1];
|
||||
while(isdigit(*tl->comment))tl->comment++;
|
||||
@ -1503,9 +1606,9 @@ static int h_ace(int argc, unsigned char **argv){
|
||||
tl->comment = strdup(tl->comment);
|
||||
|
||||
sscanf((char *)argv[1], "%u", &tl->number);
|
||||
sscanf((char *)argv[3], "%lu", &lim);
|
||||
if(sscanf((char *)argv[3], "%"SCNu64"", &lim) != 1) lim = 0;
|
||||
tl->type = getrotate(*argv[2]);
|
||||
tl->traflim64 = ((uint64_t)lim)*(1024*1024);
|
||||
tl->traflim64 = lim*(1024*1024);
|
||||
if(!tl->traflim64) {
|
||||
free(tl);
|
||||
freeacl(acl);
|
||||
@ -1801,6 +1904,9 @@ int h_server_verify(int argc, unsigned char **argv);
|
||||
int h_no_server_verify(int argc, unsigned char **argv);
|
||||
int h_client_mode(int argc, unsigned char **argv);
|
||||
#endif
|
||||
#ifdef WITH_HTTPSRV
|
||||
int h_http_content_type(int argc, unsigned char **argv);
|
||||
#endif
|
||||
#ifdef WITH_TRANSPARENT
|
||||
int h_transparent(int argc, unsigned char **argv);
|
||||
int h_notransparent(int argc, unsigned char **argv);
|
||||
@ -1826,7 +1932,8 @@ struct commands commandhandlers[]={
|
||||
#endif
|
||||
#ifdef WITH_HTTPSRV
|
||||
{NULL, "httpsrv", h_proxy, 1, 0},
|
||||
{NULL, "http", h_http, 4, 5},
|
||||
{NULL, "http", h_http, 4, 0},
|
||||
{NULL, "http_content_type", h_http_content_type, 3, 3},
|
||||
#endif
|
||||
{NULL, "dnspr", h_proxy, 1, 0},
|
||||
{NULL, "internal", h_internal, 2, 2},
|
||||
@ -1982,6 +2089,21 @@ int parsestr (unsigned char *str, unsigned char **argm, int nitems, unsigned cha
|
||||
argm[argc] = 0;
|
||||
return argc;
|
||||
case '$':
|
||||
/* Two dollars stand for one. That is how a literal dollar is
|
||||
written where a file to include would otherwise be read, and
|
||||
the second one is dropped here as a quote character is. */
|
||||
if(str[1] == '$'){
|
||||
str1 = str;
|
||||
do {
|
||||
*str1 = *(str1 + 1);
|
||||
}while(*(str1++));
|
||||
if(space){
|
||||
argm[argc++] = str;
|
||||
if(argc >= nitems) return argc;
|
||||
space = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(comment){
|
||||
if(space){
|
||||
argm[argc++] = str;
|
||||
|
||||
994
src/httpsrv.c
994
src/httpsrv.c
File diff suppressed because it is too large
Load Diff
65
src/pcre.c
65
src/pcre.c
@ -629,6 +629,71 @@ static struct symbol regexp_symbols[] = {
|
||||
};
|
||||
|
||||
|
||||
/* Compiling and matching for patterns outside the pcre commands: a host name
|
||||
or a URL in an http rule, an access rule naming a host. They go through the
|
||||
same compile, with whatever pcre_options is set to, so one kind of regular
|
||||
expression is understood everywhere.
|
||||
*/
|
||||
void * pcre_pattern_compile(const unsigned char *pattern, char *errbuf, int errlen)
|
||||
{
|
||||
pcre2_code *re;
|
||||
int errcode;
|
||||
PCRE2_SIZE erroffset;
|
||||
|
||||
re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, pcre_options,
|
||||
&errcode, &erroffset, NULL);
|
||||
if(!re){
|
||||
if(errbuf && errlen > 0){
|
||||
PCRE2_UCHAR message[256];
|
||||
|
||||
pcre2_get_error_message(errcode, message, sizeof(message));
|
||||
snprintf(errbuf, errlen, "%s at offset %d", (char *)message, (int)erroffset);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
void pcre_pattern_free(void *re)
|
||||
{
|
||||
if(re) pcre2_code_free((pcre2_code *)re);
|
||||
}
|
||||
|
||||
/* Returns the number of captures placed, or 0 when the subject does not
|
||||
match. Element 0 is the whole match. The match data is per call: a rule is
|
||||
matched from several threads at once.
|
||||
*/
|
||||
int pcre_pattern_match(void *re, const unsigned char *subject, struct capture *caps, int maxcaps)
|
||||
{
|
||||
pcre2_match_data *match_data;
|
||||
PCRE2_SIZE *ovector;
|
||||
int count, i, placed = 0;
|
||||
|
||||
if(!re || !subject) return 0;
|
||||
match_data = pcre2_match_data_create_from_pattern((pcre2_code *)re, NULL);
|
||||
if(!match_data) return 0;
|
||||
|
||||
count = pcre2_match((pcre2_code *)re, (PCRE2_SPTR)subject, PCRE2_ZERO_TERMINATED,
|
||||
0, 0, match_data, NULL);
|
||||
if(count > 0){
|
||||
ovector = pcre2_get_ovector_pointer(match_data);
|
||||
if(count > maxcaps) count = maxcaps;
|
||||
for(i = 0; i < count; i++){
|
||||
if(ovector[i*2] == PCRE2_UNSET){
|
||||
caps[i].start = 0;
|
||||
caps[i].len = 0;
|
||||
}
|
||||
else {
|
||||
caps[i].start = (int)ovector[i*2];
|
||||
caps[i].len = (int)(ovector[i*2+1] - ovector[i*2]);
|
||||
}
|
||||
}
|
||||
placed = count;
|
||||
}
|
||||
pcre2_match_data_free(match_data);
|
||||
return placed;
|
||||
}
|
||||
|
||||
void pcre_install(void){
|
||||
|
||||
struct filter *flt, *tmpflt;
|
||||
|
||||
12
src/proxy.h
12
src/proxy.h
@ -169,6 +169,7 @@ void daemonize(void);
|
||||
|
||||
#ifndef _WIN32
|
||||
size_t threadstacksize(int extra);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef WITH_ODBC
|
||||
@ -386,7 +387,18 @@ int readconfig(FILE * fp);
|
||||
void initcommands(void);
|
||||
int connectwithpoll(struct clientparam *param, SOCKET sock, struct sockaddr *sa, SASIZETYPE size, int to);
|
||||
int bindwithrange(struct clientparam *param, SOCKET sock, PROXYSOCKADDRTYPE *sa, uint32_t range);
|
||||
#ifdef WITH_PCRE
|
||||
/* One regular expression implementation for the whole program: the pcre
|
||||
commands and every pattern that carries a pcre: prefix. */
|
||||
void * pcre_pattern_compile(const unsigned char *pattern, char *errbuf, int errlen);
|
||||
void pcre_pattern_free(void *re);
|
||||
int pcre_pattern_match(void *re, const unsigned char *subject, struct capture *caps, int maxcaps);
|
||||
#endif
|
||||
|
||||
int parsepattern(struct hostname *h, unsigned char *arg);
|
||||
int parsepathpattern(struct hostname *h, unsigned char *arg);
|
||||
int patternmatchcaps(const struct hostname *h, const unsigned char *str,
|
||||
struct capture *caps, int *ncaps);
|
||||
int patternmatch(const struct hostname *h, const unsigned char *str);
|
||||
int patternmatchpos(const struct hostname *h, const unsigned char *str, int *start, int *len);
|
||||
void applyportranges(struct clientparam * param, struct ace * acentry);
|
||||
|
||||
@ -350,21 +350,47 @@ struct period {
|
||||
|
||||
#define MATCHBEGIN 1
|
||||
#define MATCHEND 2
|
||||
/* A pattern is either the star form above, matched by matchtype, or a regular
|
||||
expression compiled once when the configuration is read. */
|
||||
#define MATCHGLOB 4 /* stars anywhere: * within a path element, ** across */
|
||||
#define MATCHREGEX 5
|
||||
|
||||
/* What a star or a capturing group stood for. Element 0 is the whole
|
||||
subject, so a template writes it as $0 and the groups as $1 upwards. */
|
||||
#define MAXCAPTURES 10
|
||||
struct capture {
|
||||
int start;
|
||||
int len;
|
||||
};
|
||||
|
||||
struct hostname {
|
||||
struct hostname *next;
|
||||
unsigned char * name;
|
||||
int matchtype;
|
||||
void * re; /* compiled regular expression, MATCHREGEX only */
|
||||
};
|
||||
|
||||
/* A request handed to an http operation. */
|
||||
struct httpreq {
|
||||
struct capture caps[MAXCAPTURES];
|
||||
int ncaps;
|
||||
struct capture hostcaps[MAXCAPTURES];
|
||||
int nhostcaps;
|
||||
const char *ctype;
|
||||
const char *hdrs;
|
||||
int maxage;
|
||||
int code;
|
||||
time_t ims; /* what If-Modified-Since asked about, or 0 */
|
||||
int version; /* 0 for HTTP/1.0, 1 for HTTP/1.1 */
|
||||
int keepalive; /* whether the connection carries another request */
|
||||
int first; /* the first request on this connection */
|
||||
int chunkedreq; /* a body this server does not know how to read */
|
||||
struct clientparam *param;
|
||||
char method[16];
|
||||
char path[256];
|
||||
char query[512];
|
||||
char host[256];
|
||||
unsigned long contentlen;
|
||||
uint64_t contentlen;
|
||||
int globstart, globlen;
|
||||
};
|
||||
|
||||
@ -377,6 +403,10 @@ struct httprule {
|
||||
struct hostname url;
|
||||
int op;
|
||||
unsigned char *params;
|
||||
unsigned char *ctype; /* type named by the rule, or NULL to work it out */
|
||||
unsigned char *hdrs; /* headers the rule adds, already CRLF separated */
|
||||
int maxage; /* seconds to allow caching for, or -1 to say nothing */
|
||||
int code; /* status the rule answers with, or 0 for the usual */
|
||||
};
|
||||
|
||||
struct ace {
|
||||
|
||||
@ -21,7 +21,7 @@ def run(t):
|
||||
ssl_serv
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{secure}
|
||||
ssl_noserv"""
|
||||
|
||||
@ -30,7 +30,7 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{plain}
|
||||
{tls_origin}
|
||||
|
||||
|
||||
@ -6,14 +6,14 @@ def run(t):
|
||||
openport = t.free_port()
|
||||
t.start("httpsrv_auth", f"""
|
||||
log
|
||||
http * /echo echo
|
||||
http echo * /echo
|
||||
auth strong
|
||||
users alice:CL:secret bob:CL:hunter2
|
||||
allow alice
|
||||
httpsrv -p{srv}
|
||||
|
||||
flush
|
||||
http * /echo echo
|
||||
http echo * /echo
|
||||
auth iponly
|
||||
allow *
|
||||
httpsrv -p{openport}
|
||||
|
||||
195
tests/cases/httpsrv_files.py
Normal file
195
tests/cases/httpsrv_files.py
Normal file
@ -0,0 +1,195 @@
|
||||
"""The operations that serve a filesystem: file, cache, redir and rewrite.
|
||||
|
||||
A rule maps a request onto a path with a template, where $1 upwards stand for
|
||||
what the stars or the groups of a regular expression matched.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def run(t):
|
||||
root = os.path.join(t.tmpdir, "web")
|
||||
os.makedirs(os.path.join(root, "picts", "set"), exist_ok=True)
|
||||
with open(os.path.join(root, "a.html"), "w") as fp:
|
||||
fp.write("<h1>hello</h1>")
|
||||
with open(os.path.join(root, "big.bin"), "wb") as fp:
|
||||
fp.write(b"x" * 300000) # past a single send, and past the cache limit
|
||||
with open(os.path.join(root, "picts", "set", "dog.gif"), "wb") as fp:
|
||||
fp.write(b"GIF89a-pretend")
|
||||
|
||||
with open(os.path.join(root, "b.webp"), "wb") as fp:
|
||||
fp.write(b"RIFF-pretend")
|
||||
|
||||
port = t.free_port()
|
||||
t.start("httpsrv_files", f"""
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http rewrite * /alias/** "/w/$1"
|
||||
http file * /w/*.html "{root}/$1.html"
|
||||
http file * /big {root}/big.bin
|
||||
http cache * /c/*.html "{root}/$1.html"
|
||||
http cache * "pcre:^/(.*)/pic/(.*)\\.(gif|jpeg)$" "{root}/picts/$1/$2.$3"
|
||||
http redir * /old/** 301 "https://example.org/$1"
|
||||
http redir * /moved /w/a.html
|
||||
http file * /rel/*.html "web/$1.html"
|
||||
http echo * /echo
|
||||
|
||||
http_content_type .webp image/webp
|
||||
http_content_type dat application/x-mydata
|
||||
http file * /ct/*.webp "{root}/$1.webp"
|
||||
http file * /named/*.html "{root}/$1.html" text/x-named
|
||||
http file * /star/*.html "{root}/$1.html" *
|
||||
http cache * /ctc/*.webp "{root}/$1.webp"
|
||||
|
||||
http file * /aged/*.html "{root}/$1.html" * 3600
|
||||
http cache * /aged2/*.html "{root}/$1.html" * 60
|
||||
http file * /extra/*.html "{root}/$1.html" * * "X-One: 1\\nX-Two: two words"
|
||||
http file * /err/*.html "{root}/$1.html" * * "X-Served: static" 404
|
||||
http reply * /ok**
|
||||
http reply * /nobody** 204
|
||||
http reply * /down** 503 "Retry-After: 30"
|
||||
http cache * /held/*.html "{root}/$1.html" * 30
|
||||
httpsrv -p{port}
|
||||
""", ports=[port])
|
||||
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
|
||||
# --- file -------------------------------------------------------------
|
||||
r = t.http(url + "/w/a.html")
|
||||
t.eq(200, r.status, "a file is served")
|
||||
t.contains(r, "<h1>hello</h1>", "with its content")
|
||||
t.eq("text/html", r.header("Content-Type"), "and a type taken from the name")
|
||||
t.eq(str(len("<h1>hello</h1>")), r.header("Content-Length"), "and its length")
|
||||
|
||||
t.eq(404, t.http(url + "/w/nosuch.html").status, "a missing file is not found")
|
||||
big = t.http(url + "/big")
|
||||
t.eq(300000, big.length, "a large file arrives whole")
|
||||
t.eq("300000", big.header("Content-Length"), "and is announced by its length")
|
||||
t.eq(None, big.header("Transfer-Encoding"),
|
||||
"a file is never sent chunked")
|
||||
t.eq("300000", t.http(url + "/big", method="HEAD").header("Content-Length"),
|
||||
"HEAD gives the length without the body")
|
||||
t.eq(200, t.http(url + "/w/a.html", method="HEAD").status, "HEAD is answered")
|
||||
t.eq(0, t.http(url + "/w/a.html", method="HEAD").length, "HEAD carries no body")
|
||||
|
||||
# --- cache ------------------------------------------------------------
|
||||
first = t.http(url + "/c/a.html")
|
||||
second = t.http(url + "/c/a.html")
|
||||
t.eq(200, first.status, "a cached file is served")
|
||||
t.eq(first.text, second.text, "and the same on the next request")
|
||||
t.contains(second, "<h1>hello</h1>", "from memory this time")
|
||||
|
||||
# a file changed on disk is noticed rather than served from before
|
||||
with open(os.path.join(root, "a.html"), "w") as fp:
|
||||
fp.write("<h1>changed</h1>")
|
||||
t.contains(t.http(url + "/c/a.html"), "changed",
|
||||
"a file replaced on disk is read again")
|
||||
|
||||
# --- what the stars stand for -----------------------------------------
|
||||
r = t.http(url + "/set/pic/dog.gif")
|
||||
t.eq(200, r.status, "a regular expression maps a request onto a path")
|
||||
t.contains(r, "GIF89a", "and the file is served")
|
||||
t.eq("image/gif", r.header("Content-Type"), "with the type of that name")
|
||||
|
||||
# --- redir ------------------------------------------------------------
|
||||
r = t.http(url + "/old/thing")
|
||||
t.eq(301, r.status, "a redirect uses the status it was given")
|
||||
t.eq("https://example.org/thing", r.header("Location"),
|
||||
"and a location built from the request")
|
||||
t.eq(302, t.http(url + "/moved").status, "without a status it is 302")
|
||||
|
||||
# --- rewrite ----------------------------------------------------------
|
||||
r = t.http(url + "/alias/a.html")
|
||||
t.eq(200, r.status, "a rewritten request reaches the rule after it")
|
||||
t.contains(r, "changed", "and is served from the path it was rewritten to")
|
||||
|
||||
# --- the type a reply carries -------------------------------------------
|
||||
# Worked out from the name, using what the configuration has registered
|
||||
# on top of what is built in, unless the rule says otherwise.
|
||||
t.eq("image/webp", t.http(url + "/ct/b.webp").header("Content-Type"),
|
||||
"a registered extension names the type")
|
||||
t.eq("image/webp", t.http(url + "/ctc/b.webp").header("Content-Type"),
|
||||
"and a cached file is answered the same way")
|
||||
t.eq("text/x-named", t.http(url + "/named/a.html").header("Content-Type"),
|
||||
"a rule may name the type itself")
|
||||
t.eq("text/html", t.http(url + "/star/a.html").header("Content-Type"),
|
||||
"and a star there leaves it to the name of the file")
|
||||
|
||||
# --- what a rule adds to the answer ------------------------------------
|
||||
r = t.http(url + "/aged/a.html")
|
||||
t.eq("max-age=3600", r.header("Cache-Control"), "a rule may describe caching")
|
||||
t.eq("max-age=60", t.http(url + "/aged2/a.html").header("Cache-Control"),
|
||||
"a cached file is answered the same way")
|
||||
t.eq(None, t.http(url + "/w/a.html").header("Cache-Control"),
|
||||
"and a rule which says nothing sends nothing")
|
||||
|
||||
r = t.http(url + "/extra/a.html")
|
||||
t.eq("1", r.header("X-One"), "a rule may add headers")
|
||||
t.eq("two words", r.header("X-Two"),
|
||||
"the second of them arrives whole, spaces and all")
|
||||
|
||||
r = t.http(url + "/err/a.html")
|
||||
t.eq(404, r.status, "a rule may answer with the status it names")
|
||||
t.contains(r, "<h1>", "and the file is still the body")
|
||||
t.eq("static", r.header("X-Served"),
|
||||
"what the rule adds goes with the status the rule asked for")
|
||||
|
||||
# a refusal the server decided on is its own answer
|
||||
r = t.http(url + "/err/nosuch.html")
|
||||
t.eq(404, r.status, "a missing file is still not found")
|
||||
t.eq(None, r.header("X-Served"), "and carries none of the rule's headers")
|
||||
t.eq(None, t.http(url + "/aged/nosuch.html").header("Cache-Control"),
|
||||
"nor what it said about caching")
|
||||
|
||||
# --- reply --------------------------------------------------------------
|
||||
r = t.http(url + "/ok")
|
||||
t.eq(200, r.status, "reply answers with 200 by default")
|
||||
t.eq("0", r.header("Content-Length"), "with a length of zero")
|
||||
t.eq(0, r.length, "and no body")
|
||||
|
||||
r = t.http(url + "/nobody")
|
||||
t.eq(204, r.status, "reply answers with the status it was given")
|
||||
t.eq(None, r.header("Content-Length"),
|
||||
"and a status carrying no body is sent without a length")
|
||||
|
||||
r = t.http(url + "/down")
|
||||
t.eq(503, r.status, "reply serves a refusal the configuration decided on")
|
||||
t.eq("30", r.header("Retry-After"), "with the headers that go with it")
|
||||
|
||||
# --- a client which has the file already --------------------------------
|
||||
r = t.http(url + "/w/a.html")
|
||||
stamp = r.header("Last-Modified")
|
||||
t.ne(None, stamp, "a file is answered with the time it was last changed")
|
||||
|
||||
r = t.http(url + "/w/a.html", headers={"If-Modified-Since": stamp})
|
||||
t.eq(304, r.status, "and an unchanged file is answered 304")
|
||||
t.eq(0, r.length, "which carries no body")
|
||||
t.eq(None, r.header("Content-Length"), "and no length")
|
||||
t.eq(stamp, r.header("Last-Modified"), "but still says when the file changed")
|
||||
|
||||
t.eq(200, t.http(url + "/w/a.html",
|
||||
headers={"If-Modified-Since": "Sun, 06 Nov 1994 08:49:37 GMT"}).status,
|
||||
"an older date is answered with the file")
|
||||
t.eq(200, t.http(url + "/w/a.html",
|
||||
headers={"If-Modified-Since": "not a date at all"}).status,
|
||||
"and a date which cannot be read is treated as none")
|
||||
t.eq(304, t.http(url + "/c/a.html", headers={"If-Modified-Since": stamp}).status,
|
||||
"a file answered from memory is conditional in the same way")
|
||||
t.eq(404, t.http(url + "/err/a.html", headers={"If-Modified-Since": stamp}).status,
|
||||
"a rule with a status of its own is not turned into a 304")
|
||||
|
||||
# --- a rule which says how long its copy may be held --------------------
|
||||
t.contains(t.http(url + "/held/a.html"), "changed", "a held file is served")
|
||||
with open(os.path.join(root, "a.html"), "w") as fp:
|
||||
fp.write("<h1>replaced</h1>")
|
||||
t.not_contains(t.http(url + "/held/a.html"), "replaced",
|
||||
"and within its max-age the disk is not looked at again")
|
||||
t.contains(t.http(url + "/c/a.html"), "replaced",
|
||||
"while a rule without one notices the change at once")
|
||||
|
||||
# --- the paths a rule may not build ------------------------------------
|
||||
t.eq(403, t.http(url + "/rel/a.html").status,
|
||||
"a relative target is refused")
|
||||
t.ne(200, t.http(url + "/w/../etc/passwd").status,
|
||||
"a request climbing out of the tree is refused")
|
||||
109
tests/cases/httpsrv_keepalive.py
Normal file
109
tests/cases/httpsrv_keepalive.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""Keep-alive: which answers may be followed by another request.
|
||||
|
||||
The next request begins where the last answer ended, so a connection is only
|
||||
kept when the length of what was sent is known exactly and the body of the
|
||||
request was read to its end. Everything else closes, which is the safe way to
|
||||
be wrong.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def run(t):
|
||||
root = os.path.join(t.tmpdir, "ka")
|
||||
os.makedirs(root, exist_ok=True)
|
||||
with open(os.path.join(root, "a.html"), "w") as fp:
|
||||
fp.write("<h1>hello</h1>")
|
||||
|
||||
port = t.free_port()
|
||||
t.start("httpsrv_keepalive", f"""
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http file * /w/*.html "{root}/$1.html"
|
||||
http reply * /ok** 200
|
||||
http echo * /echo**
|
||||
http data * /chunked** size=100&chunked=1
|
||||
httpsrv -p{port}
|
||||
""", ports=[port])
|
||||
|
||||
def req(path, version="1.1", extra="", body=""):
|
||||
head = (f"GET {path} HTTP/{version}\r\nHost: t\r\n{extra}\r\n")
|
||||
if body:
|
||||
head = head.replace("GET", "POST", 1)
|
||||
return head + body
|
||||
|
||||
def session(*requests, quiet=0.5):
|
||||
text, closed = t.raw_session(port, "".join(requests), quiet=quiet)
|
||||
return text, closed, text.count("HTTP/1.")
|
||||
|
||||
# --- what keeps the connection ---------------------------------------
|
||||
text, closed, n = session(req("/w/a.html"), req("/ok"),
|
||||
req("/w/a.html", extra="Connection: close\r\n"))
|
||||
t.eq(3, n, "three 1.1 requests are answered on one connection")
|
||||
t.eq(True, closed, "and the one asking to close ends it")
|
||||
t.contains(text, "Connection: keep-alive", "the answers say the connection is kept")
|
||||
t.eq(2, text.count("<h1>hello</h1>"), "each file arrives whole")
|
||||
|
||||
text, closed, n = session(req("/w/a.html", version="1.0"), req("/ok", version="1.0"))
|
||||
t.eq(1, n, "a 1.0 request without the header is answered once")
|
||||
t.eq(True, closed, "and the connection ends")
|
||||
t.contains(text, "Connection: close", "which the answer says")
|
||||
|
||||
text, closed, n = session(req("/w/a.html", version="1.0",
|
||||
extra="Connection: keep-alive\r\n"),
|
||||
req("/ok", version="1.0",
|
||||
extra="Connection: close\r\n"))
|
||||
t.eq(2, n, "a 1.0 client asking for keep-alive gets it")
|
||||
|
||||
# a request carrying a body: the next one begins after it
|
||||
text, closed, n = session(req("/echo", extra="Content-Length: 5\r\n", body="hello"),
|
||||
req("/ok", extra="Connection: close\r\n"))
|
||||
t.eq(2, n, "a body which was read to its end leaves the stream in place")
|
||||
t.contains(text, "content.length=5", "and the body was seen")
|
||||
|
||||
# --- what ends it -----------------------------------------------------
|
||||
text, closed, n = session(req("/echo", extra="Transfer-Encoding: chunked\r\n"),
|
||||
req("/ok"))
|
||||
t.eq(1, n, "a request body this server cannot frame ends the connection")
|
||||
t.eq(True, closed, "the connection is closed rather than left mid-body")
|
||||
|
||||
# A body longer than the server is willing to read leaves the rest of it
|
||||
# in the stream, so the connection cannot carry another request. The send
|
||||
# may not even finish - the server answers and closes part way through -
|
||||
# which is the same answer from the other side.
|
||||
big = "x" * 1500000
|
||||
text, closed, n = session(req("/echo", extra="Content-Length: 1500000\r\n", body=big),
|
||||
quiet=2)
|
||||
t.eq(1, n, "a body past what the server will read is answered once")
|
||||
t.contains(text, "Connection: close",
|
||||
"and the answer ends the connection rather than leaving the rest to be read")
|
||||
|
||||
# --- answers of other shapes -----------------------------------------
|
||||
text, closed, n = session(req("/chunked"), req("/ok", extra="Connection: close\r\n"))
|
||||
t.eq(2, n, "a chunked answer may be followed by another request")
|
||||
|
||||
text, closed, n = session(req("/chunked", version="1.0"), req("/ok", version="1.0"))
|
||||
t.eq(1, n, "but not for a client which has no chunked encoding to read")
|
||||
|
||||
stamp = t.http(f"http://127.0.0.1:{port}/w/a.html").header("Last-Modified")
|
||||
text, closed, n = session(req("/w/a.html", extra=f"If-Modified-Since: {stamp}\r\n"),
|
||||
req("/ok", extra="Connection: close\r\n"))
|
||||
t.eq(2, n, "a 304 carries no body and the next request follows it")
|
||||
t.contains(text, "304", "and it is a 304")
|
||||
|
||||
# --- the administration pages always close ---------------------------
|
||||
aport = t.free_port()
|
||||
t.start("httpsrv_keepalive_admin", f"""
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http file * /w/*.html "{root}/$1.html"
|
||||
admin -p{aport}
|
||||
""", ports=[aport])
|
||||
|
||||
text, closed = t.raw_session(aport,
|
||||
f"GET /w/a.html HTTP/1.1\r\nHost: t\r\n\r\nGET /C HTTP/1.1\r\nHost: t\r\n\r\n"
|
||||
f"GET /w/a.html HTTP/1.1\r\nHost: t\r\n\r\n")
|
||||
t.eq(2, text.count("HTTP/1."), "an administration page is the last thing on a connection")
|
||||
t.eq(True, closed, "which the server closes, since the page states no length")
|
||||
@ -9,9 +9,9 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http * /small data size=64
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
http data * /small size=64
|
||||
httpsrv -p{srv}
|
||||
""", ports=[srv])
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /safe/* echo
|
||||
http echo * /echo**
|
||||
http echo * /safe/**
|
||||
httpsrv -p{srv}
|
||||
""", ports=[srv])
|
||||
|
||||
@ -62,3 +62,28 @@ def run(t):
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
t.contains(r, "method=POST", "POST reaches the handler")
|
||||
t.contains(r, "content.length=9", "the POST content length is parsed")
|
||||
|
||||
# --- dollars in the configuration ------------------------------------
|
||||
# Outside quotes a dollar begins the name of a file to include, so an
|
||||
# argument holding one is quoted. Two dollars stand for one, which is how
|
||||
# a dollar reaches a rule as text.
|
||||
dsrv = t.free_port()
|
||||
t.start("httpsrv_dollar", f"""
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http redir * /old** 301 "http://example.org/x$$y/$1"
|
||||
http redir * "pcre:^/re/([a-z]+)$" 302 "http://example.org/re/$1"
|
||||
http echo * /**
|
||||
httpsrv -p{dsrv}
|
||||
""", ports=[dsrv])
|
||||
|
||||
durl = f"http://127.0.0.1:{dsrv}"
|
||||
r = t.http(durl + "/old/a")
|
||||
t.eq(301, r.status, "a rule holding a doubled dollar loads")
|
||||
t.eq("http://example.org/x$y//a", r.header("Location"),
|
||||
"and two dollars reach the location as one")
|
||||
t.eq(302, t.http(durl + "/re/abc").status,
|
||||
"a quoted regular expression keeps its anchor")
|
||||
t.eq(200, t.http(durl + "/re/ab9").status,
|
||||
"and the anchor is real: what it excludes falls through")
|
||||
|
||||
@ -8,19 +8,25 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /exact echo
|
||||
http * /pre* echo
|
||||
http * *.suffix echo
|
||||
http * *mid* echo
|
||||
http host.example.com /byhost echo
|
||||
http *.wild.example.com /bywild echo
|
||||
http * /only-first echo
|
||||
http echo * /exact
|
||||
http echo * /pre*
|
||||
http echo * /deep/**
|
||||
http echo * **.suffix
|
||||
http echo * **mid**
|
||||
http echo host.example.com /byhost
|
||||
http echo *.wild.example.com /bywild
|
||||
http echo * /only-first
|
||||
|
||||
http rewrite_host *.old.example ** "$1.new.example"
|
||||
http rewrite_host "pcre:^legacy-(.*)$" ** "$1.new.example"
|
||||
http rewrite_host * /badhost** "not a host name"
|
||||
http echo one.new.example /**
|
||||
httpsrv -p{srv}
|
||||
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http * /only-second echo
|
||||
http echo * /only-second
|
||||
httpsrv -p{srv2}
|
||||
""", ports=[srv, srv2])
|
||||
|
||||
@ -31,8 +37,15 @@ def run(t):
|
||||
t.eq(404, t.http(url + "/exactly").status,
|
||||
"an exact URL does not match a longer path")
|
||||
t.eq(200, t.http(url + "/pre").status, "a prefix matches the bare prefix")
|
||||
t.eq(200, t.http(url + "/pretty/deep").status,
|
||||
"a prefix matches a longer path")
|
||||
t.eq(200, t.http(url + "/pretty").status,
|
||||
"a prefix matches a longer name in the same path element")
|
||||
|
||||
# a single star stays inside one element of the path, which is what keeps
|
||||
# a rule from reaching into directories it did not name
|
||||
t.eq(404, t.http(url + "/pretty/deep").status,
|
||||
"a prefix does not cross a slash")
|
||||
t.eq(200, t.http(url + "/deep/a/b/c").status,
|
||||
"a double star does cross one")
|
||||
t.eq(200, t.http(url + "/any.suffix").status, "a suffix matches")
|
||||
t.eq(404, t.http(url + "/any.suffixx").status,
|
||||
"a suffix is anchored at the end")
|
||||
@ -67,3 +80,19 @@ def run(t):
|
||||
"the second service has its own rules")
|
||||
t.eq(404, t.http(f"http://127.0.0.1:{srv2}/only-first").status,
|
||||
"the second service does not have the earlier rules")
|
||||
|
||||
# --- a rule which changes the host --------------------------------
|
||||
# The stars of the host pattern are what $1 upwards stand for here, the
|
||||
# way the stars of the URL stand for themselves in a rewrite.
|
||||
r = t.http(url + "/anything", headers={"Host": "one.old.example"})
|
||||
t.eq(200, r.status, "a rewritten host reaches the rules after it")
|
||||
t.contains(r, "host=one.new.example", "and the request carries the new name")
|
||||
|
||||
t.eq(200, t.http(url + "/anything", headers={"Host": "legacy-one"}).status,
|
||||
"a regular expression names the part to keep")
|
||||
|
||||
t.eq(404, t.http(url + "/anything", headers={"Host": "other.example"}).status,
|
||||
"a host no rule rewrites is left as it was")
|
||||
|
||||
t.eq(403, t.http(url + "/badhost", headers={"Host": "x"}).status,
|
||||
"a rule may not build something which is not a host name")
|
||||
|
||||
@ -21,8 +21,8 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
httpsrv -p{origin} -i::1
|
||||
|
||||
# reached over IPv6, and allowed to reach IPv6
|
||||
@ -102,7 +102,7 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{v4origin}
|
||||
{"".join(sections)}
|
||||
""", ports=[v4origin] + list(family_ports.values()))
|
||||
|
||||
@ -43,7 +43,7 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{srv}
|
||||
|
||||
# every outgoing connection binds inside the range
|
||||
|
||||
@ -29,9 +29,9 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /secret* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http echo * /secret**
|
||||
http data * /data
|
||||
httpsrv -p{origin}
|
||||
""", ports=[origin])
|
||||
|
||||
@ -88,6 +88,27 @@ def run(t):
|
||||
t.eq(200, t.http(url + "/echo", proxy=p).status,
|
||||
"an extension that matches nothing changes nothing")
|
||||
|
||||
# --- a regular expression where a host name is expected -----------------
|
||||
# The same prefix works in an access rule and in an http rule, so one
|
||||
# kind of expression is understood wherever a name can be written.
|
||||
named = t.free_port()
|
||||
t.start("pcre_named", f"""
|
||||
log
|
||||
flush
|
||||
nserver 127.0.0.1
|
||||
nscache 1024
|
||||
nsrecord host1.test 127.0.0.1
|
||||
nsrecord other.test 127.0.0.1
|
||||
auth iponly
|
||||
allow * * "pcre:^host[0-9]+\\.test$"
|
||||
proxy -p{named}
|
||||
""", ports=[named])
|
||||
|
||||
t.eq(200, t.http(f"http://host1.test:{origin}/echo", proxy=f"127.0.0.1:{named}").status,
|
||||
"a destination matching the expression is allowed")
|
||||
t.ne(200, t.http(f"http://other.test:{origin}/echo", proxy=f"127.0.0.1:{named}").status,
|
||||
"one that does not match is refused")
|
||||
|
||||
# --- rewriting the reply ------------------------------------------------
|
||||
p = proxy_with("rewrite_srv",
|
||||
'pcre_rewrite srvheader dunno "text/plain" "text/rewritten"',
|
||||
@ -143,7 +164,7 @@ def run(t):
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{elsewhere}
|
||||
""", ports=[elsewhere])
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
httpsrv -p{origin}
|
||||
|
||||
flush
|
||||
|
||||
@ -16,15 +16,15 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
httpsrv -p{srv}
|
||||
|
||||
# a second origin, used as a destination the rules must keep out
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{other}
|
||||
|
||||
# an open proxy
|
||||
|
||||
@ -10,8 +10,8 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
httpsrv -p{srv}
|
||||
|
||||
flush
|
||||
|
||||
@ -32,7 +32,7 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{origin}
|
||||
|
||||
flush
|
||||
@ -76,8 +76,8 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http * /data data
|
||||
http echo * /echo**
|
||||
http data * /data
|
||||
httpsrv -p{origin}
|
||||
|
||||
flush
|
||||
@ -129,7 +129,7 @@ def run(t):
|
||||
ssl_serv
|
||||
auth iponly
|
||||
allow *
|
||||
http * /secret* echo
|
||||
http echo * /secret**
|
||||
httpsrv -p{origin}
|
||||
""", ports=[origin])
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ def run(t):
|
||||
ssl_serv
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{origin}
|
||||
|
||||
flush
|
||||
|
||||
@ -97,14 +97,14 @@ def run(t):
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
http * /echo* echo
|
||||
http echo * /echo**
|
||||
httpsrv -p{origin_port} -i{ORIGIN_ADDR}
|
||||
|
||||
# a second server, to tell apart where a connection actually went
|
||||
flush
|
||||
auth iponly
|
||||
allow *
|
||||
http * * data size=13
|
||||
http data * * size=13
|
||||
httpsrv -p{decoy_port} -i{DECOY_ADDR}
|
||||
|
||||
# a port mapper aimed at the decoy: with the destination taken from
|
||||
|
||||
@ -321,6 +321,42 @@ class Tester:
|
||||
except OSError as exc:
|
||||
return f"<no reply: {exc}>"
|
||||
|
||||
def raw_session(self, port, request, host="127.0.0.1", quiet=0.5):
|
||||
"""Send bytes and read until the server closes or goes quiet.
|
||||
|
||||
Returns (text, closed). closed says the server ended the connection
|
||||
rather than leaving it open for another request, which is the whole
|
||||
question a keep-alive test asks.
|
||||
"""
|
||||
if not isinstance(request, bytes):
|
||||
request = request.encode("latin-1")
|
||||
closed = False
|
||||
chunks = []
|
||||
try:
|
||||
with socket.create_connection((host, port), self.timeout) as sock:
|
||||
try:
|
||||
sock.sendall(request)
|
||||
except OSError:
|
||||
# the server answered and closed before taking all of it,
|
||||
# which is an answer in itself
|
||||
closed = True
|
||||
sock.settimeout(quiet)
|
||||
while True:
|
||||
try:
|
||||
piece = sock.recv(65536)
|
||||
except socket.timeout:
|
||||
break # quiet: the connection is still open
|
||||
except OSError:
|
||||
closed = True # reset: it is not
|
||||
break
|
||||
if not piece:
|
||||
closed = True
|
||||
break
|
||||
chunks.append(piece)
|
||||
except OSError as exc:
|
||||
return f"<no reply: {exc}>", True
|
||||
return b"".join(chunks).decode("utf-8", "replace"), closed
|
||||
|
||||
# ---- UDP ---------------------------------------------------------
|
||||
|
||||
def udp_echo(self, prefix=b"echo:"):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user