Skip to content

Commit

Permalink
Merge pull request #1172 from bugsnag/integration/bugsnag-integrity-h…
Browse files Browse the repository at this point in the history
…eader

Add Bugsnag integrity header to Expo
  • Loading branch information
imjoehaines committed Dec 4, 2020
2 parents 0103cbe + f91879f commit e9d730c
Show file tree
Hide file tree
Showing 17 changed files with 70 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed

- (expo): Add integrity header to verify Error and Session API payloads have not changed. [#1172](https://github.com/bugsnag/bugsnag-js/pull/1172)
- (react-native): Update bugsnag-android to v5.3.0
- Add integrity header to verify Error and Session API payloads have not changed. [bugsnag-android#978](https://github.com/bugsnag/bugsnag-android/pull/978)
- (react-native): Update bugsnag-cocoa to v6.3.0
Expand Down
9 changes: 7 additions & 2 deletions packages/delivery-expo/delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const payload = require('@bugsnag/core/lib/json-payload')
const UndeliveredPayloadQueue = require('./queue')
const NetworkStatus = require('./network-status')
const RedeliveryLoop = require('./redelivery')
const Crypto = require('expo-crypto')

module.exports = (client, fetch = global.fetch) => {
const networkStatus = new NetworkStatus()
Expand Down Expand Up @@ -34,8 +35,10 @@ module.exports = (client, fetch = global.fetch) => {

const { queues, queueConsumers } = initRedelivery(networkStatus, client._logger, send)

const hash = payload => Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA1, payload)

return {
sendEvent: (event, cb = () => {}) => {
sendEvent: async (event, cb = () => {}) => {
const url = client._config.endpoints.notify

let body, opts
Expand All @@ -46,6 +49,7 @@ module.exports = (client, fetch = global.fetch) => {
headers: {
'Content-Type': 'application/json',
'Bugsnag-Api-Key': event.apiKey || client._config.apiKey,
'Bugsnag-Integrity': `sha1 ${await hash(body)}`,
'Bugsnag-Payload-Version': '4',
'Bugsnag-Sent-At': (new Date()).toISOString()
},
Expand All @@ -65,7 +69,7 @@ module.exports = (client, fetch = global.fetch) => {
}
},

sendSession: (session, cb = () => {}) => {
sendSession: async (session, cb = () => {}) => {
const url = client._config.endpoints.sessions

let body, opts
Expand All @@ -76,6 +80,7 @@ module.exports = (client, fetch = global.fetch) => {
headers: {
'Content-Type': 'application/json',
'Bugsnag-Api-Key': client._config.apiKey,
'Bugsnag-Integrity': `sha1 ${await hash(body)}`,
'Bugsnag-Payload-Version': '1',
'Bugsnag-Sent-At': (new Date()).toISOString()
},
Expand Down
5 changes: 5 additions & 0 deletions packages/delivery-expo/package-lock.json

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

3 changes: 2 additions & 1 deletion packages/delivery-expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"license": "MIT",
"dependencies": {
"@react-native-community/netinfo": "5.9.6",
"expo-file-system": "~9.2"
"expo-file-system": "~9.2",
"expo-crypto": "~8.3.0"
},
"devDependencies": {
"@bugsnag/core": "^7.5.3"
Expand Down
31 changes: 20 additions & 11 deletions packages/delivery-expo/test/delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ jest.mock('expo-file-system', () => ({
createDownloadResumable: jest.fn(() => Promise.resolve())
}))

jest.mock('expo-crypto', () => ({
CryptoDigestAlgorithm: { SHA1: 'sha1' },
digestStringAsync: jest.fn((algorithm, input) => {
if (algorithm === 'sha1') {
return Promise.resolve(input)
}

return Promise.reject(new Error(`Invalid algorithm '${algorithm}'`))
})
}))

jest.mock('@react-native-community/netinfo', () => ({
addEventListener: jest.fn(),
fetch: () => new Promise(resolve => setTimeout(() => resolve({ isConnected: true }), 1))
Expand Down Expand Up @@ -100,6 +111,7 @@ describe('delivery: expo', () => {
expect(requests[0].url).toMatch('/notify/')
expect(requests[0].headers['content-type']).toEqual('application/json')
expect(requests[0].headers['bugsnag-api-key']).toEqual('aaaaaaaa')
expect(requests[0].headers['bugsnag-integrity']).toEqual('sha1 {"events":[{"errors":[{"errorClass":"Error","errorMessage":"foo is not a function"}]}]}')
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].body).toBe(JSON.stringify(payload))
Expand Down Expand Up @@ -130,6 +142,7 @@ describe('delivery: expo', () => {
expect(requests[0].url).toMatch('/sessions/')
expect(requests[0].headers['content-type']).toEqual('application/json')
expect(requests[0].headers['bugsnag-api-key']).toEqual('aaaaaaaa')
expect(requests[0].headers['bugsnag-integrity']).toEqual('sha1 {"events":[{"errors":[{"errorClass":"Error","errorMessage":"foo is not a function"}]}]}')
expect(requests[0].headers['bugsnag-payload-version']).toEqual('1')
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].body).toBe(JSON.stringify(payload))
Expand Down Expand Up @@ -336,21 +349,17 @@ describe('delivery: expo', () => {
endpoints: { notify: 'http://some-address.com' },
redactedKeys: []
}
let n = 0
const _done = () => {
n++
if (n === 2) done()
}

const d = delivery({ _config: config, _logger: noopLogger } as unknown as Client, fetch)
d.sendEvent(payload, (err) => {
expect(err).not.toBeTruthy()
expect(enqueueSpy).toHaveBeenCalledTimes(1)
_done()
})
d.sendSession(payload, (err) => {
expect(err).not.toBeTruthy()
expect(enqueueSpy).toHaveBeenCalledTimes(2)
_done()

d.sendSession(payload, (err) => {
expect(err).not.toBeTruthy()
expect(enqueueSpy).toHaveBeenCalledTimes(2)
done()
})
})
})
})
3 changes: 2 additions & 1 deletion packages/expo/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The following modules are currently used:
- `expo-file-system`, `@react-native-community/netinfo` (`@bugsnag/delivery-expo`)
- `expo-constants` (`@bugsnag/plugin-expo-app`)
- `expo-device` (`@bugsnag/plugin-expo-device`)
- `expo-crypto` (`@bugsnag/expo`, `@bugsnag/delivery-expo`)

If you add a new dependency please add it to this list.

Expand All @@ -25,4 +26,4 @@ https://github.com/expo/expo/blob/master/changelogVersions.json

## Updating the CLI to install a compatible notifier version

When the version of the bundled native modules changes the notifier will be incompatible with previous Expo SDKs. To prevent installing the conflicting versions, we need to update the CLI using the established pattern in [`packages/expo-cli/commands/install.js`](../expo-cli/commands/install.js).
When the version of the bundled native modules changes the notifier will be incompatible with previous Expo SDKs. To prevent installing the conflicting versions, we need to update the CLI using the established pattern in [`packages/expo-cli/commands/install.js`](../expo-cli/commands/install.js).
1 change: 1 addition & 0 deletions packages/expo/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jest.mock('../../plugin-expo-app/node_modules/expo-constants', () => ({
}))

jest.mock('@bugsnag/delivery-expo')
jest.mock('../../delivery-expo/node_modules/expo-crypto', () => ({}))

jest.mock('react-native', () => ({
NativeModules: {
Expand Down
3 changes: 2 additions & 1 deletion test/expo/features/app.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Scenario: App data is included by default
And the event "app.duration" is not null
And the event "app.durationInForeground" is not null
And the event "app.inForeground" is true
And the Bugsnag-Integrity header is valid

Scenario: App data can be modified by a callback
Given the element "enhancedAppButton" is present
Expand All @@ -23,4 +24,4 @@ Scenario: App data can be modified by a callback
And the event "app.duration" is not null
And the event "app.durationInForeground" is not null
And the event "app.inForeground" is true

And the Bugsnag-Integrity header is valid
12 changes: 12 additions & 0 deletions test/expo/features/auto_breadcrumbs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Scenario: App-state breadcrumbs are captured by default
And the exception "errorClass" equals "Error"
And the exception "message" equals "defaultAppStateBreadcrumbsBehaviour"
And the event has a "state" breadcrumb named "App state changed"
And the Bugsnag-Integrity header is valid

@skip_android_5
Scenario: App-state breadcrumbs can be disabled specifically
Expand All @@ -27,6 +28,7 @@ Scenario: App-state breadcrumbs can be disabled specifically
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledAppStateBreadcrumbsBehaviour"
And the event does not have a "state" breadcrumb named "App state changed"
And the Bugsnag-Integrity header is valid

@skip_android_5
Scenario: App-state breadcrumbs are disabled with other auto-breadcrumbs
Expand All @@ -41,6 +43,7 @@ Scenario: App-state breadcrumbs are disabled with other auto-breadcrumbs
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledAllAppStateBreadcrumbsBehaviour"
And the event does not have a "state" breadcrumb named "App state changed"
And the Bugsnag-Integrity header is valid

@skip_android_5
Scenario: App-state breadcrumbs overrides auto-breadcrumbs
Expand All @@ -55,6 +58,7 @@ Scenario: App-state breadcrumbs overrides auto-breadcrumbs
And the exception "errorClass" equals "Error"
And the exception "message" equals "overrideAppStateBreadcrumbsBehaviour"
And the event has a "state" breadcrumb named "App state changed"
And the Bugsnag-Integrity header is valid

Scenario: Console breadcrumbs are captured by default
Given the element "consoleBreadcrumbs" is present
Expand All @@ -65,6 +69,7 @@ Scenario: Console breadcrumbs are captured by default
And the exception "errorClass" equals "Error"
And the exception "message" equals "defaultConsoleBreadcrumbsBehaviour"
And the event has a "log" breadcrumb named "Console output"
And the Bugsnag-Integrity header is valid

Scenario: Console breadcrumbs can be disabled explicitly
Given the element "consoleBreadcrumbs" is present
Expand All @@ -75,6 +80,7 @@ Scenario: Console breadcrumbs can be disabled explicitly
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledConsoleBreadcrumbsBehaviour"
And the event does not have a "log" breadcrumb named "Console output"
And the Bugsnag-Integrity header is valid

Scenario: Console breadcrumbs are disabled with other auto-breadcrumbs
Given the element "consoleBreadcrumbs" is present
Expand All @@ -85,6 +91,7 @@ Scenario: Console breadcrumbs are disabled with other auto-breadcrumbs
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledAllConsoleBreadcrumbsBehaviour"
And the event does not have a "log" breadcrumb named "Console output"
And the Bugsnag-Integrity header is valid

Scenario: Console breadcrumbs overrides auto-breadcrumbs
Given the element "consoleBreadcrumbs" is present
Expand All @@ -95,6 +102,7 @@ Scenario: Console breadcrumbs overrides auto-breadcrumbs
And the exception "errorClass" equals "Error"
And the exception "message" equals "overrideConsoleBreadcrumbsBehaviour"
And the event has a "log" breadcrumb named "Console output"
And the Bugsnag-Integrity header is valid

Scenario: Network breadcrumbs are captured by default
Given the element "networkBreadcrumbs" is present
Expand All @@ -107,6 +115,7 @@ Scenario: Network breadcrumbs are captured by default
And the event has a "request" breadcrumb named "XMLHttpRequest succeeded"
And the event "breadcrumbs.1.metaData.status" equals 200
And the event "breadcrumbs.1.metaData.request" equals "GET http://postman-echo.com/get"
And the Bugsnag-Integrity header is valid

Scenario: Network breadcrumbs can be disabled explicitly
Given the element "networkBreadcrumbs" is present
Expand All @@ -117,6 +126,7 @@ Scenario: Network breadcrumbs can be disabled explicitly
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledNetworkBreadcrumbsBehaviour"
And the event does not have a "request" breadcrumb named "XMLHttpRequest succeeded"
And the Bugsnag-Integrity header is valid

Scenario: Network breadcrumbs are disabled with other auto-breadcrumbs
Given the element "networkBreadcrumbs" is present
Expand All @@ -127,6 +137,7 @@ Scenario: Network breadcrumbs are disabled with other auto-breadcrumbs
And the exception "errorClass" equals "Error"
And the exception "message" equals "disabledAllNetworkBreadcrumbsBehaviour"
And the event does not have a "request" breadcrumb named "XMLHttpRequest succeeded"
And the Bugsnag-Integrity header is valid

Scenario: Network breadcrumbs overrides auto-breadcrumbs
Given the element "networkBreadcrumbs" is present
Expand All @@ -139,3 +150,4 @@ Scenario: Network breadcrumbs overrides auto-breadcrumbs
And the event has a "request" breadcrumb named "XMLHttpRequest succeeded"
And the event "breadcrumbs.0.metaData.status" equals 200
And the event "breadcrumbs.0.metaData.request" equals "GET http://postman-echo.com/get"
And the Bugsnag-Integrity header is valid
2 changes: 2 additions & 0 deletions test/expo/features/device.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Scenario: Device data is included by default
And the event "device.totalMemory" is not null
And the event "metaData.device.isDevice" is true
And the event "metaData.device.appOwnership" equals "standalone"
And the Bugsnag-Integrity header is valid

Scenario: Device data can be modified by a callback
Given the element "deviceCallbackButton" is present
Expand All @@ -47,3 +48,4 @@ Scenario: Device data can be modified by a callback
And the event "device.totalMemory" is not null
And the event "metaData.device.isDevice" is true
And the event "metaData.device.appOwnership" equals "standalone"
And the Bugsnag-Integrity header is valid
2 changes: 2 additions & 0 deletions test/expo/features/error_boundary.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Scenario: A render error is captured by an error boundary
And the exception "errorClass" equals "Error"
And the exception "message" starts with "An error has occurred in Buggy component!"
And the event "metaData.react.componentStack" is not null
And the Bugsnag-Integrity header is valid

@skip_android_7 @skip_android_8
Scenario: When a render error occurs, a fallback is presented
Expand All @@ -19,3 +20,4 @@ Scenario: When a render error occurs, a fallback is presented
Then I wait to receive a request
And the exception "errorClass" equals "Error"
And the element "errorBoundaryFallback" is present
And the Bugsnag-Integrity header is valid
4 changes: 3 additions & 1 deletion test/expo/features/handled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Scenario: Calling notify() with an Error
Then I wait to receive a request
And the exception "errorClass" equals "Error"
And the exception "message" equals "HandledError"
And the Bugsnag-Integrity header is valid

Scenario: Calling notify() with a caught Error
Given the element "handledCaughtErrorButton" is present
When I click the element "handledCaughtErrorButton"
Then I wait to receive a request
And the exception "errorClass" equals "Error"
And the exception "message" equals "HandledCaughtError"
And the exception "message" equals "HandledCaughtError"
And the Bugsnag-Integrity header is valid
3 changes: 2 additions & 1 deletion test/expo/features/manual_breadcrumbs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Scenario: Manual breadcrumbs are enabled when automatic breadcrumbs are disabled
Then I wait to receive a request
And the exception "message" equals "ManualBreadcrumbError"
And the event has a "manual" breadcrumb named "manualBreadcrumb"
And the event "breadcrumbs.0.metaData.reason" equals "testing"
And the event "breadcrumbs.0.metaData.reason" equals "testing"
And the Bugsnag-Integrity header is valid
2 changes: 2 additions & 0 deletions test/expo/features/metadata.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Scenario: Meta data can be set via the client
And the exception "errorClass" equals "Error"
And the exception "message" equals "MetadataClientError"
And the event "metaData.extra.reason" equals "metadataClientName"
And the Bugsnag-Integrity header is valid

Scenario: Meta data can be set via a callback
Given the element "metadataCallbackButton" is present
Expand All @@ -19,3 +20,4 @@ Scenario: Meta data can be set via a callback
And the exception "errorClass" equals "Error"
And the exception "message" equals "MetadataCallbackError"
And the event "metaData.extra.reason" equals "metadataCallbackName"
And the Bugsnag-Integrity header is valid
4 changes: 3 additions & 1 deletion test/expo/features/sessions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Scenario: Sessions can be automatically delivered
And the payload field "app" is not null
And the payload field "device" is not null
And the payload has a valid sessions array
And the Bugsnag-Integrity header is valid

Scenario: Sessions can be manually delivered
Given the element "manualSessionButton" is present
Expand All @@ -40,4 +41,5 @@ Scenario: Sessions can be manually delivered
And the payload field "notifier.version" is not null
And the payload field "app" is not null
And the payload field "device" is not null
And the payload has a valid sessions array
And the payload has a valid sessions array
And the Bugsnag-Integrity header is valid
2 changes: 2 additions & 0 deletions test/expo/features/unhandled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Scenario: Catching an Unhandled error
Then I wait to receive a request
And the exception "errorClass" equals "Error"
And the exception "message" equals "UnhandledError"
And the Bugsnag-Integrity header is valid

Scenario: Catching an Unhandled promise rejection
Given the element "unhandledPromiseRejectionButton" is present
When I click the element "unhandledPromiseRejectionButton"
Then I wait to receive a request
And the exception "errorClass" equals "Error"
And the exception "message" equals "UnhandledPromiseRejection"
And the Bugsnag-Integrity header is valid
2 changes: 2 additions & 0 deletions test/expo/features/user.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Scenario: User data can be set via the client
And the exception "errorClass" equals "Error"
And the exception "message" equals "UserClientError"
And the event "user.name" equals "userClientName"
And the Bugsnag-Integrity header is valid

Scenario: User data can be set via a callback
Given the element "userCallbackButton" is present
Expand All @@ -19,3 +20,4 @@ Scenario: User data can be set via a callback
And the exception "errorClass" equals "Error"
And the exception "message" equals "UserCallbackError"
And the event "user.name" equals "userCallbackName"
And the Bugsnag-Integrity header is valid

0 comments on commit e9d730c

Please sign in to comment.