Net-Blossom-Server

 view release on metacpan or  search on metacpan

lib/Net/Blossom/Server.pm  view on Meta::CPAN

same status behavior as C<handle_head_upload>.

=head2 handle_mirror

    my $response = $server->handle_mirror($request, %opts);

Handles a normalized C<PUT /mirror> request. The request body must be a JSON
object with a C<url> field. The URL must use C<http> or C<https>, have a host,
must not include userinfo, and must not include a fragment.

The server calls the configured C<mirror_fetcher> with a streaming sink and stores
origin bytes as the fetcher writes them. SHA-256 calculation and storage commit
remain owned by the server core. A missing C<mirror_fetcher> returns C<503>.
Malformed mirror requests return C<400>. Origin fetch failures, unusable fetch
results, or origin content-length mismatches return C<502>.

The fetcher may be a code reference called with C<$url> and C<< sink => $sink >>,
or an object called as C<< $fetcher->fetch_blob($url, sink => $sink) >>. It must
call C<< $sink->start(%metadata) >> before writing bytes, then call
C<< $sink->write($chunk) >> for each scalar byte chunk. C<%metadata> may include
C<type> and C<content_length>; missing C<type> defaults to

lib/Net/Blossom/Server/MirrorFetcher/HTTP.pm  view on Meta::CPAN


=head1 DESCRIPTION

C<Net::Blossom::Server::MirrorFetcher::HTTP> is a small allowlist-only fetcher
for C<PUT /mirror>. It is intentionally conservative: it does not provide public
internet mirroring by default.

The fetcher validates the URL before making a request, requires the URL host to
match one of the configured C<allowed_hosts>, disables redirects in the default
HTTP client, clears common proxy environment variables while fetching, and
enforces a maximum response size while streaming into the supplied sink.

=head1 CONSTRUCTOR

=head2 new

    my $fetcher = Net::Blossom::Server::MirrorFetcher::HTTP->new(%args);

Required arguments:

=over 4

t/04-StorageContract.t  view on Meta::CPAN

    like(dies { run_storage_contract_tests(name => 'storage', factory => sub { Local::ContractStorage->new }, bogus => 1) },
        qr/unknown argument\(s\): bogus/, 'unknown argument rejected');
};

run_storage_contract_tests(
    name    => 'in-memory contract storage',
    factory => sub { Local::ContractStorage->new },
);

run_storage_contract_tests(
    name    => 'streaming body contract storage',
    factory => sub { Local::StreamingContractStorage->new },
);

done_testing;

t/19-MirrorFetcherHTTP.t  view on Meta::CPAN

    like(dies { Net::Blossom::Server::MirrorFetcher::HTTP->new(allowed_hosts => ['cdn.example'], max_bytes => 0) },
        qr/max_bytes must be a positive integer/, 'max_bytes positive');
    like(dies { Net::Blossom::Server::MirrorFetcher::HTTP->new(allowed_hosts => ['cdn.example'], max_bytes => 1024, timeout => 0) },
        qr/timeout must be a positive integer/, 'timeout positive');
    like(dies { Net::Blossom::Server::MirrorFetcher::HTTP->new(allowed_hosts => ['cdn.example'], max_bytes => 1024, user_agent => bless {}, 'Local::NoRequest') },
        qr/user_agent must provide request/, 'user agent contract required');
    like(dies { Net::Blossom::Server::MirrorFetcher::HTTP->new(allowed_hosts => ['cdn.example'], max_bytes => 1024, bogus => 1) },
        qr/unknown argument\(s\): bogus/, 'unknown arguments rejected');
};

subtest 'fetch_blob requires a streaming sink' => sub {
    my $fetcher = Net::Blossom::Server::MirrorFetcher::HTTP->new(
        allowed_hosts => ['cdn.example'],
        max_bytes     => 1024,
        user_agent    => Local::UA->new,
    );

    like(dies { $fetcher->fetch_blob('https://cdn.example/blob.bin') },
        qr/sink is required/, 'sink required');
    like(dies { $fetcher->fetch_blob('https://cdn.example/blob.bin', sink => bless {}, 'Local::NoSink') },
        qr/sink must provide start and write/, 'sink contract required');

t/19-MirrorFetcherHTTP.t  view on Meta::CPAN

            status  => 200,
            reason  => 'OK',
            headers => { 'content-length' => 5 },
            content => 'hello',
        }),
    );

    my $sink = Local::Sink->new;
    is(error_status { $fetcher->fetch_blob('https://cdn.example/blob.bin', sink => $sink) },
        413, 'Content-Length over max rejected');
    is_deeply($sink->{chunks}, [], 'oversized Content-Length is rejected before streaming body');

    $fetcher = Net::Blossom::Server::MirrorFetcher::HTTP->new(
        allowed_hosts => ['cdn.example'],
        max_bytes     => 4,
        user_agent    => Local::UA->new(response => {
            success     => 1,
            status      => 200,
            reason      => 'OK',
            headers     => {},
            body_chunks => ['he', 'llo'],



( run in 2.637 seconds using v1.01-cache-2.11-cpan-995e09ba956 )