PAGI
view release on metacpan or search on metacpan
lib/PAGI/Middleware/Static.pm view on Meta::CPAN
await $send->({
type => 'http.response.body',
body => '',
more => 0,
});
return;
}
# Get MIME type
my $content_type = $self->_get_mime_type($file_path);
# Check for Range request (only if handle_ranges is enabled)
my $range_header = $self->{handle_ranges} ? $self->_get_header($scope, 'range') : undef;
my ($start, $end, $is_range) = $self->_parse_range($range_header, $size);
if ($is_range && !defined $start) {
# Invalid range
await $self->_send_error($send, 416, 'Range Not Satisfiable');
return;
}
# Build headers
my @headers = (
['content-type', $content_type],
['etag', $etag],
['last-modified', $self->_format_http_date($mtime)],
['accept-ranges', 'bytes'],
);
my $status;
my $body_size;
if ($is_range) {
$status = 206;
$body_size = $end - $start + 1;
push @headers, ['content-range', "bytes $start-$end/$size"];
push @headers, ['content-length', $body_size];
} else {
$status = 200;
$body_size = $size;
push @headers, ['content-length', $size];
}
# Send response start
await $send->({
type => 'http.response.start',
status => $status,
headers => \@headers,
});
# For HEAD requests, don't send body
if ($scope->{method} eq 'HEAD') {
await $send->({
type => 'http.response.body',
body => '',
more => 0,
});
return;
}
# Use file response for efficient streaming (sendfile or worker pool)
# This also enables XSendfile middleware to intercept the response
if ($is_range) {
await $send->({
type => 'http.response.body',
file => $file_path,
offset => $start,
length => $body_size,
});
}
else {
await $send->({
type => 'http.response.body',
file => $file_path,
});
}
};
}
sub _resolve_path {
my ($self, $url_path) = @_;
# Path is already URL-decoded by the server, so no decoding here
my $decoded = $url_path;
# Remove query string
$decoded =~ s/\?.*//;
# Combine with root (use manual concat to preserve .. for security check)
my $root = $self->{root};
$root =~ s{/$}{}; # Remove trailing slash from root
return $root . $decoded;
}
sub _is_safe_path {
my ($self, $file_path) = @_;
my $root = $self->{root};
# Manually resolve the path to handle .. without requiring file to exist
my $abs_path = $self->_resolve_dots($file_path);
return 0 unless defined $abs_path;
# Normalize both paths
$abs_path =~ s{/+}{/}g;
$root =~ s{/+}{/}g;
$root =~ s{/$}{};
$abs_path =~ s{/$}{};
# Path must start with root
return $abs_path =~ m{^\Q$root\E(?:/|$)};
}
sub _resolve_dots {
my ($self, $path) = @_;
# Split path into components
my @parts = split m{/}, $path;
my @resolved;
for my $part (@parts) {
lib/PAGI/Middleware/Static.pm view on Meta::CPAN
# Validate range
return (undef, undef, 1) if $start > $end || $start >= $size;
$end = $size - 1 if $end >= $size;
return ($start, $end, 1);
}
return (undef, undef, 0);
}
sub _format_http_date {
my ($self, $epoch) = @_;
my @days = qw(Sun Mon Tue Wed Thu Fri Sat);
my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
my @t = gmtime($epoch);
return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
$days[$t[6]], $t[3], $months[$t[4]], $t[5] + 1900,
$t[2], $t[1], $t[0]);
}
async sub _send_error {
my ($self, $send, $status, $message) = @_;
await $send->({
type => 'http.response.start',
status => $status,
headers => [
['content-type', 'text/plain'],
['content-length', length($message)],
],
});
await $send->({
type => 'http.response.body',
body => $message,
more => 0,
});
}
1;
__END__
=head1 SECURITY
This middleware includes path traversal protection to prevent access to
files outside the configured root directory. Requests containing ".."
sequences that would escape the root are rejected with 403 Forbidden.
=head1 CACHING
The middleware generates ETags based on file path, size, and modification
time. Clients can use If-None-Match to receive 304 Not Modified responses
when the file hasn't changed.
=head1 RANGE REQUESTS
The middleware supports HTTP Range requests for partial content, useful
for resumable downloads and media streaming. Only single byte ranges
are supported (not multi-range requests).
=head1 SEE ALSO
L<PAGI::Middleware> - Base class for middleware
=cut
( run in 1.001 second using v1.01-cache-2.11-cpan-140bd7fdf52 )