Skip to content

Commit

Permalink
fix(utils): Fail silently if the provided Dsn is invalid
Browse files Browse the repository at this point in the history
  • Loading branch information
Lms24 committed May 15, 2023
1 parent 5440807 commit b419764
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 91 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export function getReportDialogEndpoint(
},
): string {
const dsn = makeDsn(dsnLike);
if (!dsn) {
return '';
}

const endpoint = `${getBaseApiEndpoint(dsn)}embed/error-page/`;

let encodedOptions = `dsn=${dsnToString(dsn)}`;
Expand Down
14 changes: 8 additions & 6 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
this._options = options;
if (options.dsn) {
this._dsn = makeDsn(options.dsn);
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
this._transport = options.transport({
recordDroppedEvent: this.recordDroppedEvent.bind(this),
...options.transportOptions,
url,
});
if (this._dsn) {
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
this._transport = options.transport({
recordDroppedEvent: this.recordDroppedEvent.bind(this),
...options.transportOptions,
url,
});
}
} else {
__DEBUG_BUILD__ && logger.warn('No DSN provided, client will not do anything.');
}
Expand Down
21 changes: 16 additions & 5 deletions packages/utils/src/dsn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DsnComponents, DsnLike, DsnProtocol } from '@sentry/types';

import { SentryError } from './error';
import { logger } from './logger';

/** Regular expression used to parse a Dsn. */
const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/;
Expand Down Expand Up @@ -100,9 +101,19 @@ function validateDsn(dsn: DsnComponents): boolean | void {
return true;
}

/** The Sentry Dsn, identifying a Sentry instance and project. */
export function makeDsn(from: DsnLike): DsnComponents {
const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from);
validateDsn(components);
return components;
/**
* Creates a valid Sentry Dsn object, identifying a Sentry instance and project.
* @returns a valid DsnComponents object or `undefined` if @param from is an invalid DSN source
*/
export function makeDsn(from: DsnLike): DsnComponents | undefined {
try {
const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from);
validateDsn(components);
return components;
} catch (e) {
if (e instanceof SentryError) {
logger.error(e.message);
}
return undefined;
}
}
173 changes: 93 additions & 80 deletions packages/utils/test/dsn.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { dsnToString, makeDsn } from '../src/dsn';
import { SentryError } from '../src/error';
import { logger } from '../src/logger';

function testIf(condition: boolean): jest.It {
return condition ? test : test.skip;
}

const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});

describe('Dsn', () => {
beforeEach(() => {
loggerSpy.mockClear();
});

describe('fromComponents', () => {
test('applies all components', () => {
const dsn = makeDsn({
Expand All @@ -16,13 +22,13 @@ describe('Dsn', () => {
protocol: 'https',
publicKey: 'abc',
});
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('xyz');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('1234');
expect(dsn.path).toBe('');
expect(dsn.projectId).toBe('123');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('xyz');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('1234');
expect(dsn?.path).toBe('');
expect(dsn?.projectId).toBe('123');
});

test('applies partial components', () => {
Expand All @@ -32,169 +38,176 @@ describe('Dsn', () => {
protocol: 'https',
publicKey: 'abc',
});
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('');
expect(dsn.path).toBe('');
expect(dsn.projectId).toBe('123');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('');
expect(dsn?.path).toBe('');
expect(dsn?.projectId).toBe('123');
});

testIf(__DEBUG_BUILD__)('throws for missing components', () => {
expect(() =>
testIf(__DEBUG_BUILD__)('returns `undefined` for missing components', () => {
expect(
makeDsn({
host: '',
projectId: '123',
protocol: 'https',
publicKey: 'abc',
}),
).toThrow(SentryError);
expect(() =>
).toBeUndefined();
expect(
makeDsn({
host: 'sentry.io',
projectId: '',
protocol: 'https',
publicKey: 'abc',
}),
).toThrow(SentryError);
expect(() =>
).toBeUndefined();
expect(
makeDsn({
host: 'sentry.io',
projectId: '123',
protocol: '' as 'http', // Trick the type checker here
publicKey: 'abc',
}),
).toThrow(SentryError);
expect(() =>
).toBeUndefined();
expect(
makeDsn({
host: 'sentry.io',
projectId: '123',
protocol: 'https',
publicKey: '',
}),
).toThrow(SentryError);
).toBeUndefined();

expect(logger.error).toHaveBeenCalledTimes(4);
});

testIf(__DEBUG_BUILD__)('throws for invalid components', () => {
expect(() =>
testIf(__DEBUG_BUILD__)('returns `undefined` if components are invalid', () => {
expect(
makeDsn({
host: 'sentry.io',
projectId: '123',
protocol: 'httpx' as 'http', // Trick the type checker here
publicKey: 'abc',
}),
).toThrow(SentryError);
expect(() =>
).toBeUndefined();
expect(
makeDsn({
host: 'sentry.io',
port: 'xxx',
projectId: '123',
protocol: 'https',
publicKey: 'abc',
}),
).toThrow(SentryError);
).toBeUndefined();

expect(logger.error).toHaveBeenCalledTimes(2);
});
});

describe('fromString', () => {
test('parses a valid full Dsn', () => {
const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123');
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('xyz');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('1234');
expect(dsn.path).toBe('');
expect(dsn.projectId).toBe('123');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('xyz');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('1234');
expect(dsn?.path).toBe('');
expect(dsn?.projectId).toBe('123');
});

test('parses a valid partial Dsn', () => {
const dsn = makeDsn('https://abc@sentry.io/123/321');
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('');
expect(dsn.path).toBe('123');
expect(dsn.projectId).toBe('321');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('');
expect(dsn?.path).toBe('123');
expect(dsn?.projectId).toBe('321');
});

test('parses a Dsn with empty password', () => {
const dsn = makeDsn('https://abc:@sentry.io/123/321');
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('');
expect(dsn.path).toBe('123');
expect(dsn.projectId).toBe('321');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('');
expect(dsn?.path).toBe('123');
expect(dsn?.projectId).toBe('321');
});

test('with a long path', () => {
const dsn = makeDsn('https://abc@sentry.io/sentry/custom/installation/321');
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('');
expect(dsn.path).toBe('sentry/custom/installation');
expect(dsn.projectId).toBe('321');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('');
expect(dsn?.path).toBe('sentry/custom/installation');
expect(dsn?.projectId).toBe('321');
});

test('with a query string', () => {
const dsn = makeDsn('https://abc@sentry.io/321?sample.rate=0.1&other=value');
expect(dsn.protocol).toBe('https');
expect(dsn.publicKey).toBe('abc');
expect(dsn.pass).toBe('');
expect(dsn.host).toBe('sentry.io');
expect(dsn.port).toBe('');
expect(dsn.path).toBe('');
expect(dsn.projectId).toBe('321');
expect(dsn?.protocol).toBe('https');
expect(dsn?.publicKey).toBe('abc');
expect(dsn?.pass).toBe('');
expect(dsn?.host).toBe('sentry.io');
expect(dsn?.port).toBe('');
expect(dsn?.path).toBe('');
expect(dsn?.projectId).toBe('321');
});

testIf(__DEBUG_BUILD__)('throws when provided invalid Dsn', () => {
expect(() => makeDsn('some@random.dsn')).toThrow(SentryError);
testIf(__DEBUG_BUILD__)('returns undefined when provided invalid Dsn', () => {
expect(makeDsn('some@random.dsn')).toBeUndefined();
expect(logger.error).toHaveBeenCalledTimes(1);
});

testIf(__DEBUG_BUILD__)('throws without mandatory fields', () => {
expect(() => makeDsn('://abc@sentry.io/123')).toThrow(SentryError);
expect(() => makeDsn('https://@sentry.io/123')).toThrow(SentryError);
expect(() => makeDsn('https://abc@123')).toThrow(SentryError);
expect(() => makeDsn('https://abc@sentry.io/')).toThrow(SentryError);
testIf(__DEBUG_BUILD__)('returns undefined if mandatory fields are missing', () => {
expect(makeDsn('://abc@sentry.io/123')).toBeUndefined();
expect(makeDsn('https://@sentry.io/123')).toBeUndefined();
expect(makeDsn('https://abc@123')).toBeUndefined();
expect(makeDsn('https://abc@sentry.io/')).toBeUndefined();
expect(logger.error).toHaveBeenCalledTimes(4);
});

testIf(__DEBUG_BUILD__)('throws for invalid fields', () => {
expect(() => makeDsn('httpx://abc@sentry.io/123')).toThrow(SentryError);
expect(() => makeDsn('httpx://abc@sentry.io:xxx/123')).toThrow(SentryError);
expect(() => makeDsn('http://abc@sentry.io/abc')).toThrow(SentryError);
testIf(__DEBUG_BUILD__)('returns undefined if fields are invalid', () => {
expect(makeDsn('httpx://abc@sentry.io/123')).toBeUndefined();
expect(makeDsn('httpx://abc@sentry.io:xxx/123')).toBeUndefined();
expect(makeDsn('http://abc@sentry.io/abc')).toBeUndefined();
expect(logger.error).toHaveBeenCalledTimes(3);
});
});

describe('toString', () => {
test('excludes the password by default', () => {
const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123');
expect(dsnToString(dsn)).toBe('https://abc@sentry.io:1234/123');
expect(dsnToString(dsn!)).toBe('https://abc@sentry.io:1234/123');
});

test('optionally includes the password', () => {
const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123');
expect(dsnToString(dsn, true)).toBe('https://abc:xyz@sentry.io:1234/123');
expect(dsnToString(dsn!, true)).toBe('https://abc:xyz@sentry.io:1234/123');
});

test('renders no password if missing', () => {
const dsn = makeDsn('https://abc@sentry.io:1234/123');
expect(dsnToString(dsn, true)).toBe('https://abc@sentry.io:1234/123');
expect(dsnToString(dsn!, true)).toBe('https://abc@sentry.io:1234/123');
});

test('renders no port if missing', () => {
const dsn = makeDsn('https://abc@sentry.io/123');
expect(dsnToString(dsn)).toBe('https://abc@sentry.io/123');
expect(dsnToString(dsn!)).toBe('https://abc@sentry.io/123');
});

test('renders the full path correctly', () => {
const dsn = makeDsn('https://abc@sentry.io/sentry/custom/installation/321');
expect(dsnToString(dsn)).toBe('https://abc@sentry.io/sentry/custom/installation/321');
expect(dsnToString(dsn!)).toBe('https://abc@sentry.io/sentry/custom/installation/321');
});
});
});

0 comments on commit b419764

Please sign in to comment.