PAGI

 view release on metacpan or  search on metacpan

lib/PAGI/Middleware/Static.pm  view on Meta::CPAN

            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) {
        if ($part eq '' || $part eq '.') {
            # Skip empty and current dir
            next;
        } elsif ($part eq '..') {
            # Go up one directory
            if (@resolved) {
                pop @resolved;
            }
            # If we can't go up, the path is invalid (would escape root)
        } else {
            push @resolved, $part;
        }
    }

    # Reconstruct absolute path
    return '/' . join('/', @resolved);
}

sub _find_index {
    my ($self, $dir_path) = @_;

    for my $index (@{$self->{index}}) {
        my $index_path = File::Spec->catfile($dir_path, $index);
        return $index_path if -f $index_path;
    }
    return;
}

sub _generate_etag {
    my ($self, $file_path, $size, $mtime) = @_;

    my $data = "$file_path:$size:$mtime";
    return '"' . md5_hex($data) . '"';
}

sub _get_mime_type {
    my ($self, $file_path) = @_;

    my ($ext) = $file_path =~ /\.([^.]+)$/;
    $ext = lc($ext // '');
    return $MIME_TYPES{$ext} // 'application/octet-stream';
}

sub _get_header {
    my ($self, $scope, $name) = @_;

    $name = lc($name);
    for my $h (@{$scope->{headers} // []}) {
        return $h->[1] if lc($h->[0]) eq $name;
    }
    return;
}

sub _parse_range {
    my ($self, $range_header, $size) = @_;

    return (undef, undef, 0) unless $range_header;



( run in 0.570 second using v1.01-cache-2.11-cpan-71847e10f99 )