Skip to content
Merged
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
57 changes: 8 additions & 49 deletions src/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
objectKeyFromArtifactUrl,
streamToString,
toSemverRange,
verifyHash,
} from "./helpers";
import { z, ZodError } from "zod";
import {
Expand Down Expand Up @@ -686,39 +685,19 @@ export const RetrieveLatestSystemRecovery = cachedRedirect(
recovery.file,
);

const [firmwareFile, hashFile] = await Promise.all([
// TODO: store file hash using custom header to avoid extra request
s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: artifactPath,
}),
),
s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: `${artifactPath}.sha256`,
}),
),
]);

if (!firmwareFile.Body || !hashFile.Body) {
throw new NotFoundError(
`Recovery image or hash file not found for version ${latestVersion}`,
);
if (!(await s3ObjectExists(artifactPath))) {
throw new NotFoundError(`Recovery image not found for version ${latestVersion}`);
}

await verifyHash(firmwareFile, hashFile, "recovery image hash does not match");

console.log("recovery image hash matches", latestVersion);

return `${baseUrl}/${artifactPath}`;
},
);

/**
* 302 to the newest over-the-air artifact of one kind for the requested SKU,
* after verifying the object against its .sha256 sibling. The product table
* 302 to the newest over-the-air artifact of one kind for the requested SKU.
* Integrity is checked at publish time (the .sha256 sidecar is written by the
* release script, the sync script verifies hash and signature); here the
* object only has to exist. The product table
* says which prefix holds it, so the same URL serves every product. Used by
* build tooling (rv1106-system pulls the app binary into the system image)
* and by flashing scripts.
Expand Down Expand Up @@ -763,30 +742,10 @@ function latestArtifactRedirect(kind: OtaKind) {
artifact.file,
);

const [artifactFile, hashFile] = await Promise.all([
s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: artifactPath,
}),
),
s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: `${artifactPath}.sha256`,
}),
),
]);

if (!artifactFile.Body || !hashFile.Body) {
throw new NotFoundError(
`${prefix} artifact or hash file not found for version ${latestVersion}`,
);
if (!(await s3ObjectExists(artifactPath))) {
throw new NotFoundError(`${prefix} artifact not found for version ${latestVersion}`);
}

await verifyHash(artifactFile, hashFile, `${prefix} hash does not match`);

console.log(`${prefix} hash matches`, latestVersion);
return `${baseUrl}/${artifactPath}`;
},
);
Expand Down
63 changes: 10 additions & 53 deletions test/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ function mockS3LegacyVersionWithContent(
Contents: [],
});

// Legacy artifact exists (HeadObjectCommand for the existence check)
s3Mock.on(HeadObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({});

// Mock legacy file path with content
s3Mock.on(GetObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({
Body: createAsyncIterable(content) as any,
Expand Down Expand Up @@ -929,25 +932,6 @@ describe("RetrieveLatestApp S3 redirect handler", () => {
);
});

it("should throw InternalServerError when hash does not match", async () => {
const req = createMockRequest({});
const res = createMockResponse();

s3Mock.on(ListObjectsV2Command, { Prefix: "app/" }).resolves({
CommonPrefixes: [{ Prefix: "app/1.0.0/" }],
});

mockS3LegacyVersionWithContent(
"app",
"1.0.0",
"jetkvm_app",
"actual-content",
"wrong-hash-value",
);

await expect(RetrieveLatestApp(req, res)).rejects.toThrow(InternalServerError);
});

it("should throw NotFoundError when app file is missing", async () => {
const req = createMockRequest({});
const res = createMockResponse();
Expand All @@ -961,12 +945,9 @@ describe("RetrieveLatestApp S3 redirect handler", () => {
Contents: [],
});

s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app" }).resolves({
Body: undefined,
});
s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app.sha256" }).resolves({
Body: createAsyncIterable("some-hash") as any,
});
s3Mock
.on(HeadObjectCommand, { Key: "app/1.0.0/jetkvm_app" })
.rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } });

await expect(RetrieveLatestApp(req, res)).rejects.toThrow(NotFoundError);
});
Expand Down Expand Up @@ -1304,28 +1285,7 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => {
);
});

it("should throw InternalServerError when hash does not match", async () => {
const req = createMockRequest({});
const res = createMockResponse();

s3Mock.on(ListObjectsV2Command, { Prefix: "system/" }).resolves({
CommonPrefixes: [{ Prefix: "system/1.0.0/" }],
});

mockS3LegacyVersionWithContent(
"system",
"1.0.0",
"update.img",
"actual-content",
"mismatched-hash",
);

await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow(
InternalServerError,
);
});

it("should throw NotFoundError when recovery image or hash file is missing", async () => {
it("should throw NotFoundError when recovery image is missing", async () => {
const req = createMockRequest({});
const res = createMockResponse();

Expand All @@ -1338,12 +1298,9 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => {
Contents: [],
});

s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img" }).resolves({
Body: undefined,
});
s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img.sha256" }).resolves({
Body: undefined,
});
s3Mock
.on(HeadObjectCommand, { Key: "system/1.0.0/update.img" })
.rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } });

await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow(NotFoundError);
});
Expand Down
Loading