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
79 changes: 79 additions & 0 deletions __tests__/resolve-payload-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,42 @@ describe('run', () => {
)
})

it('inflates a wrapped compressed-payload envelope', async () => {
const envelope = {
type: 'compressed-payload',
data: gzipSync(JSON.stringify(payload)).toString('base64'),
pullRequestNumber: 123
}
const core = await runWith(JSON.stringify(JSON.stringify(envelope)))

expect(core.setFailed).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith(
'client_payload mode=compressed-envelope'
)
expect(outputsOf(core).cm_repository).toBe('acme/cm-repo')
})

it('fails loudly when a compressed-payload envelope has no gzip data', async () => {
const core = await runWith(
JSON.stringify(
JSON.stringify({ type: 'compressed-payload', data: 'not-gzip' })
)
)

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('carries no gzip data')
)
})

it('treats a raw payload carrying its own type as a raw payload', async () => {
const core = await runWith(JSON.stringify({ ...payload, type: 'push' }))

expect(core.setFailed).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith('client_payload mode=plain')
expect(outputsOf(core).github_token).toBe('ghs_token')
expect(outputsOf(core).cm_repository).toBe('acme/cm-repo')
})

it('fails on a payload that is not valid JSON', async () => {
const core = await runWith('not json')

Expand Down Expand Up @@ -153,6 +189,20 @@ describe('run with an oversized-payload reference', () => {
expect(outputsOf(core).cm_repository).toBe('acme/cm-repo')
})

it('fetches from a double-encoded reference envelope', async () => {
const fetchMock = mockFetch({
ok: true,
text: async () => JSON.stringify(payload)
})

const core = await runWith(JSON.stringify(JSON.stringify(reference)))

expect(core.setFailed).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith('client_payload mode=reference')
expect(fetchMock).toHaveBeenCalled()
expect(outputsOf(core).cm_repository).toBe('acme/cm-repo')
})

it('inflates a stashed payload that is gzipped', async () => {
mockFetch({
ok: true,
Expand All @@ -165,6 +215,35 @@ describe('run with an oversized-payload reference', () => {
expect(outputsOf(core).cm_repo_ref).toBe('main')
})

it('fails loudly when the stash returns neither gzip nor JSON', async () => {
// The stash holds the payload, not the envelope, and its form depends on
// whether compression won: bare base64(gzip) if it did, raw JSON if not.
// Anything else must be an error rather than a fall-through.
mockFetch({ ok: true, text: async () => 'not-json-not-gzip' })

const core = await runWith(JSON.stringify(reference))

expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('Failed resolving client payload')
)
})

it('names the offending URL when payloadUrl is not absolute', async () => {
const fetchMock = mockFetch({ ok: true, text: async () => '{}' })

const core = await runWith(
JSON.stringify({
...reference,
payloadUrl: '/api/v1/gitstream/payload/k'
})
)

expect(fetchMock).not.toHaveBeenCalled()
expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('stashed payload URL is not absolute')
)
})

it('refuses an origin other than the resolver', async () => {
const fetchMock = mockFetch({ ok: true, text: async () => '{}' })

Expand Down
48 changes: 33 additions & 15 deletions scripts/resolve-payload-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
const { gunzipSync } = require('zlib')

const OVERSIZED_PAYLOAD_REFERENCE = 'oversized-payload-reference'
const COMPRESSED_PAYLOAD = 'compressed-payload'
const PAYLOAD_FETCH_TIMEOUT_MS = 10000

// 32MB
Expand Down Expand Up @@ -51,26 +52,35 @@ function parsePayload(value) {
}

/**
* @returns {object | null} the stash reference, or null for a regular payload
* @returns {object | null} the parsed value, or null when `raw` is not JSON at
* all - the bare base64(gzip) form, which has no envelope around it
*/
function readStashReference(raw) {
// Cheap pre-check so a regular payload is only parsed once, further down.
if (!raw.includes(OVERSIZED_PAYLOAD_REFERENCE)) {
function tryParsePayload(raw) {
try {
const parsed = parsePayload(raw)
return parsed && typeof parsed === 'object' ? parsed : null
} catch {
return null
}
const parsed = parsePayload(raw)
return parsed && parsed.type === OVERSIZED_PAYLOAD_REFERENCE ? parsed : null
}

// Builds the stash URL on the resolver's own origin.
function stashUrl(payloadUrl, resolverUrl) {
if (!resolverUrl) {
throw new Error(
'resolver_url is not set; cannot validate the stashed payload origin'
)
}
const resolverOrigin = new URL(resolverUrl).origin
const requested = new URL(payloadUrl)
let requested
try {
// The trigger always sends an absolute URL; both it and resolver_url are
// built from the same base, so a relative one means that base was empty.
requested = new URL(payloadUrl)
} catch {
throw new Error(
`stashed payload URL is not absolute: ${payloadUrl} - the resolver's public API base is probably unset`
)
}
if (requested.origin !== resolverOrigin) {
throw new Error(
`refusing to fetch stashed payload from ${requested.origin}; expected ${resolverOrigin}`
Expand All @@ -96,19 +106,27 @@ async function fetchStashedPayload(reference, resolverUrl, core) {
return parsePayload(inflateIfGzipped(body) ?? body)
}

/**
* @returns {Promise<{ mode: string, payload: object }>}
*/
async function resolvePayload(raw, resolverUrl, core) {
const reference = readStashReference(raw)
if (reference) {
const payload = await fetchStashedPayload(reference, resolverUrl, core)
return { mode: 'reference', payload }
const parsed = tryParsePayload(raw)
if (parsed) {
if (parsed.type === OVERSIZED_PAYLOAD_REFERENCE) {
const payload = await fetchStashedPayload(parsed, resolverUrl, core)
return { mode: 'reference', payload }
}
if (parsed.type === COMPRESSED_PAYLOAD) {
const inflated = inflateIfGzipped(parsed.data || '')
if (inflated === null) {
throw new Error(`${COMPRESSED_PAYLOAD} envelope carries no gzip data`)
Comment thread
yeelali14 marked this conversation as resolved.
}
return { mode: 'compressed-envelope', payload: parsePayload(inflated) }
}
return { mode: 'plain', payload: parsed }
}
const inflated = inflateIfGzipped(raw)
if (inflated !== null) {
return { mode: 'compressed', payload: parsePayload(inflated) }
}
// Not JSON and not gzip - let the JSON error describe what arrived.
return { mode: 'plain', payload: parsePayload(raw) }
}

Expand Down