diff --git a/package.json b/package.json index 819f83b8..e3a9447b 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/main/transports/net.ts b/src/main/transports/net.ts index 324dfb55..6245e82f 100644 --- a/src/main/transports/net.ts +++ b/src/main/transports/net.ts @@ -16,6 +16,7 @@ const GZIP_THRESHOLD = 1024 * 32; */ export interface SentryElectronRequest extends Omit { body: string | Buffer; + contentType: string; } /** @@ -71,6 +72,7 @@ export class NetTransport extends Transports.BaseTransport { return this.sendRequest({ body: bodyBuffer, + contentType: 'application/x-sentry-envelope', url: this._api.getEnvelopeEndpointWithUrlEncodedAuth(), type, }); @@ -113,6 +115,12 @@ export class NetTransport extends Transports.BaseTransport { return this._buffer.add( new Promise((resolve, reject) => { + const options = this._getRequestOptions(new url.URL(request.url)); + options.headers = { + ...options.headers, + 'Content-Type': request.contentType, + }; + const req = net.request(options as Electron.ClientRequestConstructorOptions); req.on('error', reject); req.on('response', (res: Electron.IncomingMessage) => { diff --git a/src/main/uploader.ts b/src/main/uploader.ts index e611a429..4e2d5b45 100644 --- a/src/main/uploader.ts +++ b/src/main/uploader.ts @@ -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. @@ -275,6 +278,40 @@ export class MinidumpUploader { event: Event, minidumpPath: string, ): Promise { + 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 @@ -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, @@ -311,6 +351,7 @@ export class MinidumpUploader { return { body: bodyBuffer, + contentType: 'application/x-sentry-envelope', url: this._api.getEnvelopeEndpointWithUrlEncodedAuth(), type: 'event', }; diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts index c04292c8..1bb6ab00 100644 --- a/test/e2e/index.test.ts +++ b/test/e2e/index.test.ts @@ -176,11 +176,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); @@ -203,12 +199,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 @@ -268,7 +259,7 @@ 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() { const electronPath = await downloadElectron(version, arch); context = new TestContext(electronPath, join(__dirname, 'preload-app')); await context.start(); @@ -296,13 +287,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); diff --git a/test/e2e/server.ts b/test/e2e/server.ts index 4b9f4697..58e7a41b 100644 --- a/test/e2e/server.ts +++ b/test/e2e/server.ts @@ -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. */ @@ -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 @@ -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)); }); diff --git a/yarn.lock b/yarn.lock index 7f3882f9..2e3d7a49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -231,6 +231,16 @@ "@types/qs" "*" "@types/range-parser" "*" +"@types/express@*": + version "4.17.9" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz#f5f2df6add703ff28428add52bdec8a1091b0a78" + integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/express@^4.17.3": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" @@ -275,6 +285,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/multer@^1.4.4": + version "1.4.4" + resolved "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz#bb5d9abc410da82726ceca74008bb81813349a88" + integrity sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ== + dependencies: + "@types/express" "*" + "@types/multiparty@^0.0.31": version "0.0.31" resolved "https://registry.yarnpkg.com/@types/multiparty/-/multiparty-0.0.31.tgz#301fd2db05a5d4f3408613eb5b9652cd610b79e2" @@ -462,6 +479,11 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -599,6 +621,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -765,7 +795,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.2: +concat-stream@^1.5.2, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -969,6 +999,14 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -2312,6 +2350,20 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2401,7 +2453,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1: +object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -2449,7 +2501,7 @@ object.values@^1.1.1: function-bind "^1.1.1" has "^1.0.3" -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -2809,6 +2861,16 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +readable-stream@1.1.x, readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^2.2.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -2822,16 +2884,6 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -3155,6 +3207,11 @@ sshpk@^1.7.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -3477,7 +3534,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -3618,6 +3675,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + xtend@~2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"