FU: HTTP date handling + fu->send_file

This commit is contained in:
Yorhel 2025-02-24 11:12:01 +01:00
parent 18e642290d
commit 8595c4ba64
2 changed files with 144 additions and 8 deletions

100
FU.pm
View file

@ -208,7 +208,7 @@ sub _monitor {
our $hdrname_re = qr/[!#\$\%&'\*\+-\.^_`\|~0-9a-zA-Z]{1,127}/;
our $method_re = qr/(?:GET|POST|DELETE|OPTIONS|PUT|PATCH|QUERY)/;
our $method_re = qr/(?:HEAD|GET|POST|DELETE|OPTIONS|PUT|PATCH|QUERY)/;
# rfc7230 used as reference, though strict conformance is not a goal.
# Does not limit size of headers, so not suitable for deployment in untrusted networks.
@ -303,7 +303,7 @@ sub _log_err($e) {
}
sub _do_req($c) {
local $REQ = { hdr => {} };
local $REQ = { hdr => {}, trace_start => time };
local $fu = bless {}, 'FU::obj';
$REQ->{ip} = $c->{client_sock} isa 'IO::Socket::INET' ? $c->{client_sock}->peerhost : '127.0.0.1';
@ -316,7 +316,8 @@ sub _do_req($c) {
for my $h (@before_request) { $h->() }
my $path = fu->path;
my $r = $path_routes{ fu->method }{$path};
my $method = fu->method eq 'HEAD' ? 'GET' : fu->method;
my $r = $path_routes{$method}{$path};
if ($r) { $r->() }
else {
for $r ($re_routes{ fu->method }->@*) {
@ -567,6 +568,7 @@ sub run(%conf) {
package FU::obj;
use v5.36;
use Carp 'confess';
sub fu() { $FU::fu }
@ -655,7 +657,37 @@ sub add_header($, $hdr, $val) {
sub set_header($, $hdr, $val=undef) {
_validate_header($hdr, $val);
$FU::REQ{reshdr}{ lc $hdr } = $val;
$FU::REQ->{reshdr}{ lc $hdr } = $val;
}
sub send_file($, $root, $path) {
# This also catches files with '..' somewhere in the middle of the name.
# Let's just disallow that to simplify this check, I'd err on the side of
# caution.
return if $path =~ /\.\./;
my $fn = "$root/$path";
my $m = (stat $fn)[9];
return if !defined $m;
fu->set_header('last-modified', FU::Util::httpdate_format($m));
my $ims = fu->header('if-modified-since');
$ims = FU::Util::httpdate_parse($ims) if $ims;
if ($ims && $ims > $m) {
fu->status(304);
fu->done;
}
my $ctype = FU::mime_types->{$path =~ m{\.([^/\.]+)$} ? lc $1 : ''};
{
open my $fh, '<', $fn or confess "Unable to open '$fn': $!";
local $/=undef;
my $body = <$fh>;
$ctype ||= substr($body, 0, 1024) =~ /[\x00-\x08\x0e-\x1f]/ ? 'application/octet-stream' : 'text/plain';
fu->set_body($body);
}
fu->set_header('content-type', $ctype);
fu->done;
}
sub _error_page($, $code, $title, $msg) {
@ -687,9 +719,10 @@ sub _finalize {
state $haszstd = eval { require Compress::Zstd; 1 };
my $r = $FU::REQ;
if ($r->{status} == 204) {
if ($r->{status} == 204 || $r->{status} == 304) {
delete $r->{reshdr}{'content-length'};
delete $r->{reshdr}{'content-encoding'};
delete $r->{reshdr}{'content-type'};
$r->{resbody} = '';
} else {
@ -716,7 +749,7 @@ sub _finalize {
$r->{resbody} = '' if (fu->method//'') eq 'HEAD';
}
$r->{reshdr}{'content-type'} .= '; charset=UTF-8' if FU::utf8_mimes->{$r->{reshdr}{'content-type'}};
$r->{reshdr}{'content-type'} .= '; charset=UTF-8' if FU::utf8_mimes->{ $r->{reshdr}{'content-type'}||'' };
}
sub _flush($, $sock) {
@ -729,6 +762,8 @@ sub _flush($, $sock) {
$sock->print("\r\n");
} else {
$sock->printf("HTTP/1.0 %d Hello\r\n", $r->{status});
$sock->printf("date: %s\r\n", FU::Util::httpdate_format time);
$sock->print("server: FU\r\n");
}
for my ($hdr, $val) ($r->{reshdr}->%*) {
@ -898,6 +933,21 @@ setting to enable or disable debugging features in your own code.
Enable logging of requests that took longer than C<$ms> milliseconds to
process. Can be set to 0 to disable such logging.
=item FU::mime_types
Returns a modifiable hashref that serves as a lookup table from file extension
to MIME type, used by C<< fu->send_file() >>.
=item FU::utf8_mimes
Returns a modifiable hashref listing which mime types should get a UTF-8
C<charset> parameter appended to them in the C<Content-Type> header.
=item FU::compress_mimes
Returns a modifiable hashref listing mime types for which compression makes
sense.
=item FU::monitor_path(@paths)
Add filesystem paths to be monitored for changes when running in monitor mode
@ -1150,6 +1200,42 @@ templating system or L<FU::XMLWriter>:
};
});
=item fu->send_file($root, $path)
If a file identified by C<"$root/$path"> exists, set that as response and call
C<< fu->done >>. Returns normally if the file does not exist. This method is
mainly intended to serve small static files from a directory:
FU::before_request {
# We can set custom headers before send_file()
fu->set_header('cache-control', 'max-age=31536000');
# Attempt to serve files from '/static/files'
fu->send_file('/static/files', fu->path);
# If that fails, fall back to another directory
fu->send_file('/more/static/files', fu->path);
# Otherwise, continue processing the request as normal
fu->reset;
};
C<$path> may be an untrusted string from the client, this method prevents path
traversal attacks that go below the given C<$root>. It does follow symlinks,
though.
This method loads the entire file contents in memory and does not support range
requests, so DO NOT use it to send large files. Actual web servers are much
more efficient at sending static files.
The content-type header is determined from the file extension in C<$path>,
using the configured C<FU::mime_types>. As fallback, files that look like they
might be text get C<text/plain> and binary files are served with
C<application/octet-stream>.
This method sets an appropriate C<last-modified> header and supports
conditional requests with C<if-modified-since>.
=back
I<TODO:> Setting cookies.
@ -1158,8 +1244,6 @@ I<TODO:> JSON output.
I<TODO:> Redirection responses.
I<TODO:> Sending files.
=head2 Running the Site