Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/lib/frontend/handlers/pubapi.client.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 88 additions & 0 deletions app/lib/frontend/handlers/pubapi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:_pub_shared/data/package_api.dart';
import 'package:_pub_shared/data/publisher_api.dart';
import 'package:_pub_shared/data/task_api.dart';
import 'package:api_builder/api_builder.dart';
import 'package:chunked_stream/chunked_stream.dart' show MaximumSizeExceeded;
import 'package:pub_dev/database/database.dart';
import 'package:pub_dev/frontend/handlers/atom_feed.dart';
import 'package:shelf/shelf.dart';
Expand All @@ -21,6 +22,7 @@ import '../../publisher/backend.dart';
import '../../shared/exceptions.dart';
import '../../shared/handlers.dart';
import '../../shared/urls.dart' as urls;
import '../../shared/utils.dart' show canonicalizeVersion, ByteFolderExt;
import '../../task/backend.dart' show taskBackend;
import 'account.dart';
import 'cache_control.dart';
Expand Down Expand Up @@ -80,6 +82,49 @@ class PubApi {
);
}

/// Fetches the Sigstore attestation bundle for a specific (package, version) pair.
/// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#fetching-attestations
///
/// The attestation is also exported to the exported API bucket (see
/// `ExportedPackage.attestation`), from where it is meant to be served
/// directly, like the version listing and the archives. This handler
/// serves the canonical copy for hosts that do not route the path there.
@EndPoint.get('/api/packages/<package>/versions/<version>/attestation')
Future<Response> getPackageVersionAttestation(
Request request,
String package,
String version,
) async {
checkPackageVersionParams(package, version);
final canonicalVersion = canonicalizeVersion(version);
InvalidInputException.checkSemanticVersion(canonicalVersion);
// Moderated or deleted packages and versions must not expose their
// attestation either.
final pv = await packageBackend.lookupPackageVersion(
package,
canonicalVersion!,
);
if (pv == null ||
pv.isNotVisible ||
!await packageBackend.isPackageVisible(package)) {
throw NotFoundException.resource('version "$package $version"');
}
final bytes = await packageBackend.readCanonicalBucketAttestation(
package,
canonicalVersion,
);
Comment thread
mosuem marked this conversation as resolved.
if (bytes == null) {
throw NotFoundException.resource('attestation for $package $version');
}
return Response.ok(
bytes,
headers: {
'content-type': attestationContentType,
...CacheControl.clientApi.headers,
},
);
}

/// Downloading package.
///
/// This is the endpoint we link to from the version listing.
Expand Down Expand Up @@ -138,7 +183,50 @@ class PubApi {
// integration tests before we switch traffic.
await packageBackend.startUpload(
request.requestedUri.resolve('/api/packages/versions/newUploadFinish'),
attestationUrlPrefix: request.requestedUri.resolve(
'/api/packages/versions/newUploadAttestation',
),
);

/// Upload an attestation for a package archive that is being uploaded.
///
/// The attestation is uploaded before the package archive, and is stored
/// next to the archive when the upload is finished.
/// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-with-an-attestation
///
/// POST /api/packages/versions/newUploadAttestation/<uploadId>
/// <attestation JSON bundle>
/// [200 OK]
/// {
/// "success" : {
/// "message": "Attestation uploaded.",
/// },
/// }
@EndPoint.post('/api/packages/versions/newUploadAttestation/<uploadId>')
Future<SuccessMessage> uploadPackageAttestation(
Request request,
String uploadId,
) async {
final List<int> bytes;
try {
// Stop reading as soon as the body is too large, instead of buffering
// the whole request before checking its size.
bytes = await request.read().foldBytes(
maxSize: maxAttestationContentLength,
);
} on MaximumSizeExceeded {
throw PackageRejectedException.attestationTooLarge(
maxAttestationContentLength,
);
}
if (bytes.isEmpty) {
throw PackageRejectedException(
'Invalid attestation bundle format: the request body is empty.',
);
}
await packageBackend.uploadAttestation(uploadId, bytes);
Comment thread
mosuem marked this conversation as resolved.
return SuccessMessage(success: Message(message: 'Attestation uploaded.'));
}

/// Finish async upload.
/// TODO: Link to the spec once it has the details updated:
Expand Down
35 changes: 35 additions & 0 deletions app/lib/frontend/handlers/pubapi.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 18 additions & 8 deletions app/lib/package/api_export/api_exporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ final class ApiExporter {
/// * Version listing for [package] is up-to-date,
/// * Advisories for [package] is up-to-date,
/// * Tarballs for each version of [package] is up-to-date,
/// * Delete tarballs from old versions that no-longer exist.
/// * Attestations for each attested version of [package] is up-to-date,
/// * Delete tarballs and attestations from old versions that no-longer exist.
///
/// This is intended when:
/// * Running a full background synchronization.
Expand Down Expand Up @@ -238,15 +239,14 @@ final class ApiExporter {
);

if (!skipArchives) {
final versions = await packageBackend.listVersionsInCanonicalBucket(
package,
);
final (:versions, :attestations) = await packageBackend
.listVersionsAndAttestationsInCanonicalBucket(package);

// Remove versions that are not exposed in the public API.
versions.removeWhere(
(version, _) =>
!versionListing.versions.any((v) => v.version == version),
);
bool isListed(String version) =>
versionListing.versions.any((v) => v.version == version);
versions.removeWhere((version, _) => !isListed(version));
attestations.removeWhere((version, _) => !isListed(version));

await _api
.package(package)
Expand All @@ -255,6 +255,16 @@ final class ApiExporter {
forceWrite: forceWrite,
forceDelete: forceDelete,
);
// Note: this must happen before the version listing is written below,
// as clients fetch the attestation of a version when the listing
// says there is one (and fail the download if it is missing).
await _api
.package(package)
.synchronizeAttestations(
attestations,
forceWrite: forceWrite,
forceDelete: forceDelete,
);
}

await _api
Expand Down
Loading
Loading