Skip to content

Commit

Permalink
set Bugsnag-Integrity header in delivery-xml-http-request
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Skinner committed Oct 16, 2024
1 parent af9731d commit c81a572
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 24 deletions.
6 changes: 4 additions & 2 deletions packages/browser/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ describe('browser notifier', () => {
done()
})

session.onreadystatechange()
notify.onreadystatechange()
process.nextTick(() => {
session.onreadystatechange()
notify.onreadystatechange()
}, 1000)
})

it('does not send an event with invalid configuration', () => {
Expand Down
63 changes: 42 additions & 21 deletions packages/delivery-xml-http-request/delivery.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
const payload = require('@bugsnag/core/lib/json-payload')

async function getIntegrity (windowOrWorkerGlobalScope, requestBody) {
if (windowOrWorkerGlobalScope.isSecureContext && windowOrWorkerGlobalScope.crypto && windowOrWorkerGlobalScope.crypto.subtle && windowOrWorkerGlobalScope.crypto.subtle.digest && typeof TextEncoder === 'function') {
const msgUint8 = new TextEncoder().encode(requestBody)
const hashBuffer = await windowOrWorkerGlobalScope.crypto.subtle.digest('SHA-1', msgUint8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('')

return 'sha1 ' + hashHex
}
}

module.exports = (client, win = window) => ({
sendEvent: (event, cb = () => {}) => {
try {
Expand All @@ -11,31 +24,39 @@ module.exports = (client, win = window) => ({
const req = new win.XMLHttpRequest()
const body = payload.event(event, client._config.redactedKeys)

req.onreadystatechange = function () {
if (req.readyState === win.XMLHttpRequest.DONE) {
const status = req.status
if (status === 0 || status >= 400) {
const err = new Error(`Request failed with status ${status}`)
client._logger.error('Event failed to send…', err)
if (body.length > 10e5) {
client._logger.warn(`Event oversized (${(body.length / 10e5).toFixed(2)} MB)`)
getIntegrity(win, body).then((integrity) => {
req.onreadystatechange = function () {
if (req.readyState === win.XMLHttpRequest.DONE) {
const status = req.status
if (status === 0 || status >= 400) {
const err = new Error(`Request failed with status ${status}`)
client._logger.error('Event failed to send…', err)
if (body.length > 10e5) {
client._logger.warn(`Event oversized (${(body.length / 10e5).toFixed(2)} MB)`)
}
cb(err)
} else {
cb(null)
}
cb(err)
} else {
cb(null)
}
}
}
req.open('POST', url)
req.setRequestHeader('Content-Type', 'application/json')
req.setRequestHeader('Bugsnag-Api-Key', event.apiKey || client._config.apiKey)
req.setRequestHeader('Bugsnag-Payload-Version', '4')
req.setRequestHeader('Bugsnag-Sent-At', (new Date()).toISOString())
if (url.substring(0, 5) === 'https') {
req.setRequestHeader('Access-Control-Max-Age', 86400)
}
if (integrity) {
req.setRequestHeader('Bugsnag-Integrity', integrity)
}

req.open('POST', url)
req.setRequestHeader('Content-Type', 'application/json')
req.setRequestHeader('Bugsnag-Api-Key', event.apiKey || client._config.apiKey)
req.setRequestHeader('Bugsnag-Payload-Version', '4')
req.setRequestHeader('Bugsnag-Sent-At', (new Date()).toISOString())
if (url.substring(0, 5) === 'https') {
req.setRequestHeader('Access-Control-Max-Age', 86400)
}
req.send(body)
req.send(body)
}).catch((err) => {
client._logger.error(err)
cb(err)
})
} catch (e) {
client._logger.error(e)
}
Expand Down
58 changes: 57 additions & 1 deletion packages/delivery-xml-http-request/test/delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('delivery:XMLHttpRequest', () => {
redactedKeys: []
}

delivery({ _logger: {}, _config: config } as unknown as Client, { XMLHttpRequest } as unknown as Window).sendEvent(payload, (err: any) => {
delivery({ _logger: {}, _config: config } as unknown as Client, { ...window, XMLHttpRequest, isSecureContext: true } as unknown as Window).sendEvent(payload, (err: any) => {
expect(err).toBe(null)
expect(requests.length).toBe(1)
expect(requests[0].method).toBe('POST')
Expand All @@ -67,6 +67,62 @@ describe('delivery:XMLHttpRequest', () => {
expect(requests[0].headers['Bugsnag-Payload-Version']).toEqual('4')
expect(requests[0].headers['Bugsnag-Sent-At']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
expect(requests[0].headers['Access-Control-Max-Age']).toEqual(86400)
expect(requests[0].headers['Bugsnag-Integrity']).toEqual('sha1 14faf2461b0519f9d9d62cfb8d79483fcc8f825c')
expect(requests[0].data).toBe(JSON.stringify(payload))
done()
})
})

it('omits the bugsnag integrity header when not in a secure context', done => {
const requests: MockXMLHttpRequest[] = []

// mock XMLHttpRequest class
function XMLHttpRequest (this: MockXMLHttpRequest) {
this.method = null
this.url = null
this.data = null
this.headers = {}
this.readyState = XMLHttpRequest.UNSENT
requests.push(this)
}
XMLHttpRequest.UNSENT = 0
XMLHttpRequest.OPENED = 1
XMLHttpRequest.HEADERS_RECEIVED = 2
XMLHttpRequest.LOADING = 3
XMLHttpRequest.DONE = 4
XMLHttpRequest.prototype.open = function (method: string, url: string) {
this.method = method
this.url = url
this.readyState = XMLHttpRequest.OPENED
this.onreadystatechange()
}
XMLHttpRequest.prototype.setRequestHeader = function (key: string, val: string) {
this.headers[key] = val
}
XMLHttpRequest.prototype.send = function (data: string) {
this.data = data
this.readyState = XMLHttpRequest.HEADERS_RECEIVED
this.onreadystatechange()

setTimeout(() => {
this.status = 200
this.readyState = XMLHttpRequest.DONE
this.onreadystatechange()
}, 500)
}

const payload = { sample: 'payload' } as unknown as EventDeliveryPayload
const config = {
apiKey: 'aaaaaaaa',
endpoints: { notify: 'https/echo/' },
redactedKeys: []
}

delivery({ _logger: {}, _config: config } as unknown as Client, { ...window, XMLHttpRequest, isSecureContext: false } as unknown as Window).sendEvent(payload, (err: any) => {
expect(err).toBe(null)
expect(requests.length).toBe(1)
expect(requests[0].method).toBe('POST')
expect(requests[0].headers['Bugsnag-Integrity']).toBeUndefined()
expect(requests[0].data).toBe(JSON.stringify(payload))
done()
})
Expand Down

0 comments on commit c81a572

Please sign in to comment.