Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle multipart/form-data submissions on native crashes in Linux #278

Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -56,6 +56,7 @@
"@types/finalhandler": "^0.0.32",
"@types/form-data": "^2.5.0",
"@types/mocha": "^5.2.5",
"@types/multer": "^1.4.4",
"@types/multiparty": "^0.0.31",
"@types/node-fetch": "^1.6.9",
"@types/stack-trace": "^0.0.29",
Expand All @@ -73,6 +74,7 @@
"finalhandler": "^1.1.1",
"http-router": "^0.5.0",
"mocha": "^6.2.2",
"multer": "^1.4.2",
"npm-run-all": "^4.1.5",
"prettier": "^1.19.1",
"rimraf": "^2.6.2",
Expand Down
4 changes: 3 additions & 1 deletion src/main/transports/net.ts
Expand Up @@ -11,6 +11,7 @@ import { isAppReady } from '../backend';
*/
export interface SentryElectronRequest extends Omit<SentryRequest, 'body'> {
body: string | Buffer;
contentType: string;
}

/** Using net module of electron */
Expand Down Expand Up @@ -53,6 +54,7 @@ export class NetTransport extends Transports.BaseTransport {

return this.sendRequest({
body: bodyBuffer,
contentType: 'application/x-sentry-envelope',
url: this._api.getEnvelopeEndpointWithUrlEncodedAuth(),
type,
});
Expand Down Expand Up @@ -84,7 +86,7 @@ export class NetTransport extends Transports.BaseTransport {
const options = this._getRequestOptions(new url.URL(request.url));
options.headers = {
...options.headers,
'Content-Type': 'application/x-sentry-envelope',
'Content-Type': request.contentType,
};
const req = net.request(options as Electron.ClientRequestConstructorOptions);
req.on('error', reject);
Expand Down
43 changes: 42 additions & 1 deletion src/main/uploader.ts
Expand Up @@ -17,6 +17,9 @@ const MAX_REQUESTS_COUNT = 10;
/** Supported types of Electron CrashReporters. */
type CrashReporterType = 'crashpad' | 'breakpad';

/** Regex for multipart/form-data boundary */
const multipartBoundaryRegex = /^--(-+[a-zA-Z0-9]+)/;

/**
* Payload for a minidump request comprising a persistent file system path and
* event metadata.
Expand Down Expand Up @@ -275,6 +278,40 @@ export class MinidumpUploader {
event: Event,
minidumpPath: string,
): Promise<SentryElectronRequest> {
let minidumpContent: Buffer | null = null;
// We only need to handle multipart/form-data submissions on linux if
// attachments aren't ratelimited. We should send a typical sentry envelope
// if we're ratelimited.
if (process.platform === 'linux' && !transport.isRateLimited('attachment')) {
const maybeMinidumpContent = (await readFileAsync(minidumpPath)) as Buffer;
// Check to see if the buffer is a multipart/form-data submission
// if it is we will add our event as a new part to the submission
// and send that request instead of a typical sentry envelope
const first256Chars = maybeMinidumpContent.toString('utf8', 0, 256);
const multipartBoundaryInfo = multipartBoundaryRegex.exec(first256Chars);
if (multipartBoundaryInfo) {
const boundary = multipartBoundaryInfo[1];
const contentDisposition = 'Content-Disposition: form-data; name="sentry"';
const contentType = 'Content-Type: application/json';
const payload = JSON.stringify(event);
const body = Buffer.concat([
Buffer.from(`--${boundary}\r\n${contentDisposition}\r\n${contentType}\r\n\r\n${payload}\r\n`),
maybeMinidumpContent,
]);

return {
body,
contentType: `multipart/form-data; boundary=${boundary}`,
url: MinidumpUploader.minidumpUrlFromDsn(this._api.getDsn()),
type: 'event',
};
} else {
// Fall-through to default behavior if the content is not a multipart/form-data
// submission.
minidumpContent = maybeMinidumpContent;
}
}

const envelopeHeaders = JSON.stringify({
event_id: event.event_id,
// Internal helper that uses `perf_hooks` to get clock reading
Expand All @@ -296,7 +333,10 @@ export class MinidumpUploader {

// Only add attachment if they are not rate limited
if (!transport.isRateLimited('attachment')) {
const minidumpContent = (await readFileAsync(minidumpPath)) as Buffer;
if (!minidumpContent) {
minidumpContent = (await readFileAsync(minidumpPath)) as Buffer;
}

const minidumpHeader = JSON.stringify({
attachment_type: 'event.minidump',
length: minidumpContent.length,
Expand All @@ -311,6 +351,7 @@ export class MinidumpUploader {

return {
body: bodyBuffer,
contentType: 'application/x-sentry-envelope',
url: this._api.getEnvelopeEndpointWithUrlEncodedAuth(),
type: 'event',
};
Expand Down
31 changes: 10 additions & 21 deletions test/e2e/index.test.ts
Expand Up @@ -12,8 +12,7 @@ const SENTRY_KEY = '37f8a2ee37c0409d8970bc7559c7c7e4';
should();
use(chaiAsPromised);

const tests = getTests('1.8.8', '2.0.18', '3.1.13', '4.2.12', '5.0.13', '6.1.7', '7.1.11', '8.3.0', '9.0.5');
// const tests = getTests('1.8.8');
const tests = getTests('4.2.12', '5.0.13', '6.1.12', '7.3.3', '8.5.5', '9.3.5', '10.1.6', '11.0.4');

describe('E2E Tests', () => {
let testServer: TestServer;
Expand Down Expand Up @@ -157,11 +156,7 @@ describe('E2E Tests', () => {
});

// tslint:disable-next-line
it('Native crash in renderer process', async function() {
if (majorVersion === 9 && process.platform === 'linux') {
this.skip();
return;
}
it('Native crash in renderer process', async () => {
await context.start('sentry-basic', 'native-renderer');
// It can take rather a long time to get the event on Mac
await context.waitForEvents(testServer, 1, 20000);
Expand All @@ -184,12 +179,7 @@ describe('E2E Tests', () => {
});

// tslint:disable-next-line
it('Native crash in main process', async function() {
if (majorVersion === 9 && process.platform === 'linux') {
// TODO: Check why this fails on linux
this.skip();
return;
}
it('Native crash in main process', async () => {
await context.start('sentry-basic', 'native-main');

// wait for the main process to die
Expand Down Expand Up @@ -250,7 +240,12 @@ describe('E2E Tests', () => {
expect(event.data.fingerprint).to.include('abcd');
});

it('Loaded via preload script with nodeIntegration disabled', async () => {
it('Loaded via preload script with nodeIntegration disabled', async function() {
if (majorVersion < 8) {
timfish marked this conversation as resolved.
Show resolved Hide resolved
this.skip();
return;
}

const electronPath = await downloadElectron(version, arch);
context = new TestContext(electronPath, join(__dirname, 'preload-app'));
await context.start();
Expand Down Expand Up @@ -278,13 +273,7 @@ describe('E2E Tests', () => {
expect(breadcrumbs.length).to.greaterThan(4);
});

// tslint:disable-next-line
it('Custom release string for minidump', async function() {
if (majorVersion === 9 && process.platform === 'linux') {
// TODO: Check why this fails on linux
this.skip();
return;
}
it('Custom release string for minidump', async () => {
await context.start('sentry-custom-release', 'native-renderer');
// It can take rather a long time to get the event on Mac
await context.waitForEvents(testServer, 1, 20000);
Expand Down
24 changes: 24 additions & 0 deletions test/e2e/server.ts
Expand Up @@ -4,6 +4,7 @@ import { Event } from '@sentry/types';
import bodyParser = require('body-parser');
import express = require('express');
import finalhandler = require('finalhandler');
import multer = require('multer');
import { createServer, Server } from 'http';

/** Event payload that has been submitted to the test server. */
Expand Down Expand Up @@ -31,6 +32,7 @@ export class TestServer {

/** Starts accepting requests. */
public start(): void {
const upload = multer({ storage: multer.memoryStorage() });
const app = express();
app.use(
// eslint-disable-next-line deprecation/deprecation
Expand Down Expand Up @@ -64,6 +66,28 @@ export class TestServer {
res.end('Success');
});

app.post('/api/:id/minidump', upload.fields([{ name: 'upload_file_minidump' }]), (req, res) => {
const auth = (req.headers['x-sentry-auth'] as string) || '';
const keyMatch = auth.match(/sentry_key=([a-f0-9]*)/);
if (!keyMatch) {
res.status(400);
res.end('Missing authentication header');
return;
}

const files = req.files as { [fieldName: string]: Express.Multer.File[] };

this.events.push({
data: JSON.parse(req.body.sentry) as Event,
dump_file: Boolean(files.upload_file_minidump[0]),
id: req.params.id,
sentry_key: keyMatch[1],
});

res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('Success');
});

this._server = createServer((req, res) => {
app(req as any, res as any, finalhandler(req, res));
});
Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-app/fixtures/sentry-basic.js
@@ -1,6 +1,7 @@
const { init } = require('../../../../');

init({
appName: 'test-app',
dsn: process.env.DSN,
debug: true,
onFatalError: _error => {
Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-app/fixtures/sentry-custom-release.js
@@ -1,6 +1,7 @@
const { init } = require('../../../../');

init({
appName: 'test-app',
dsn: process.env.DSN,
debug: true,
release: 'some-custom-release',
Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-app/fixtures/sentry-custom-renderer-name.js
@@ -1,6 +1,7 @@
const { init } = require('../../../../');

init({
appName: 'test-app',
dsn: process.env.DSN,
debug: true,
onFatalError: _error => {
Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-app/fixtures/sentry-onfatal-exit.js
Expand Up @@ -4,6 +4,7 @@ const { app } = require('electron');
const { init } = require('../../../../');

init({
appName: 'test-app',
dsn: process.env.DSN,
debug: true,
onFatalError: _error => {
Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-app/fixtures/sentry-scope-user-data.js
@@ -1,6 +1,7 @@
const { init, configureScope } = require('../../../../');

init({
appName: 'test-app',
dsn: process.env.DSN,
onFatalError: _error => {
// We need this here otherwise we will get a dialog and travis will be stuck
Expand Down