Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ export const mysql_transaction_isolation_level = getIsolationLevelStatement(
GetConvarInt('mysql_transaction_isolation_level', 2),
);

// split on the first separator only, so values (e.g. passwords) may contain it
function splitOnce(str: string, separator: string): [string, string | undefined] {
const index = str.indexOf(separator);
return index === -1 ? [str, undefined] : [str.slice(0, index), str.slice(index + 1)];
}

function decodeUriComponent(str?: string) {
if (!str) return str;

try {
return decodeURIComponent(str);
} catch {
// not valid percent-encoding (e.g. a literal '%'); keep the raw value
return str;
}
}

function parseUri(connectionString: string) {
const splitMatchGroups = connectionString.match(
new RegExp(
Expand All @@ -54,18 +71,18 @@ function parseUri(connectionString: string) {

if (!splitMatchGroups) throw new Error(`mysql_connection_string structure was invalid (${connectionString})`);

const authTarget = splitMatchGroups[2] ? splitMatchGroups[2].split(':') : [];
const [user, password] = splitMatchGroups[2] ? splitOnce(splitMatchGroups[2], ':') : [];

const options = {
user: authTarget[0] || undefined,
password: authTarget[1] || undefined,
user: decodeUriComponent(user) || undefined,
password: decodeUriComponent(password) || undefined,
host: splitMatchGroups[3],
port: parseInt(splitMatchGroups[4]),
database: splitMatchGroups[5]?.replace(/^\/+/, ''),
...(splitMatchGroups[6] &&
splitMatchGroups[6].split('&').reduce<Record<string, string>>((connectionInfo, parameter) => {
const [key, value] = parameter.split('=');
connectionInfo[key] = value;
const [key, value] = splitOnce(parameter, '=');
connectionInfo[key] = value as string;
return connectionInfo;
}, {})),
};
Expand All @@ -85,8 +102,8 @@ export function getConnectionOptions(connectionString: string = mysql_connection
.replace(/(?:db)=/gi, 'database=')
.split(';')
.reduce<Record<string, string>>((connectionInfo, parameter) => {
const [key, value] = parameter.split('=');
if (key) connectionInfo[key] = value;
const [key, value] = splitOnce(parameter, '=');
if (key) connectionInfo[key] = value as string;
return connectionInfo;
}, {});

Expand Down
25 changes: 25 additions & 0 deletions tests/unit/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ describe('getConnectionOptions', () => {
expect(opts.database).toBe('mydb');
});

test('keeps colons in a URI password', () => {
const opts = config.getConnectionOptions('mysql://root:pa:ss@localhost/mydb') as any;
expect(opts).toMatchObject({ user: 'root', password: 'pa:ss', host: 'localhost', database: 'mydb' });
});

test('decodes percent-encoded URI credentials', () => {
const opts = config.getConnectionOptions('mysql://us%40er:p%40ss%23w%2Frd%3F@localhost/mydb') as any;
expect(opts).toMatchObject({ user: 'us@er', password: 'p@ss#w/rd?', host: 'localhost', database: 'mydb' });
});

test('keeps URI credentials that are not valid percent-encoding as-is', () => {
const opts = config.getConnectionOptions('mysql://root:100%@localhost/mydb') as any;
expect(opts.password).toBe('100%');
});

test('keeps equals signs in URI query parameter values', () => {
const opts = config.getConnectionOptions('mysql://localhost/mydb?charset=utf8mb4&foo=a=b') as any;
expect(opts).toMatchObject({ charset: 'utf8mb4', foo: 'a=b' });
});

test('keeps equals signs in a key-value password', () => {
const opts = config.getConnectionOptions('host=localhost;user=root;password=ab=cd==;database=app') as any;
expect(opts).toMatchObject({ host: 'localhost', user: 'root', password: 'ab=cd==', database: 'app' });
});

test('normalises key-value aliases (server/uid/pwd/db) to canonical keys', () => {
const opts = config.getConnectionOptions('server=127.0.0.1;uid=root;pwd=secret;db=app') as any;
expect(opts).toMatchObject({ host: '127.0.0.1', user: 'root', password: 'secret', database: 'app' });
Expand Down