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 )