Use libsignal-client validation for nicknames
This commit is contained in:
parent
f46b93d806
commit
5d07167222
7 changed files with 56 additions and 145 deletions
|
@ -1,7 +1,11 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { usernames } from '@signalapp/libsignal-client';
|
import {
|
||||||
|
usernames,
|
||||||
|
LibSignalErrorBase,
|
||||||
|
ErrorCode,
|
||||||
|
} from '@signalapp/libsignal-client';
|
||||||
|
|
||||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
@ -102,6 +106,35 @@ export async function reserveUsername(
|
||||||
return reserveUsername(options);
|
return reserveUsername(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (error instanceof LibSignalErrorBase) {
|
||||||
|
if (
|
||||||
|
error.code === ErrorCode.CannotBeEmpty ||
|
||||||
|
error.code === ErrorCode.NicknameTooShort
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ReserveUsernameError.NotEnoughCharacters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error.code === ErrorCode.NicknameTooLong) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ReserveUsernameError.TooManyCharacters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error.code === ErrorCode.CannotStartWithDigit) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ReserveUsernameError.CheckStartingCharacter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error.code === ErrorCode.BadNicknameCharacter) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ReserveUsernameError.CheckCharacters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,6 @@ import {
|
||||||
} from '../../types/Username';
|
} from '../../types/Username';
|
||||||
import * as usernameServices from '../../services/username';
|
import * as usernameServices from '../../services/username';
|
||||||
import type { ReserveUsernameResultType } from '../../services/username';
|
import type { ReserveUsernameResultType } from '../../services/username';
|
||||||
import {
|
|
||||||
isValidNickname,
|
|
||||||
getMinNickname,
|
|
||||||
getMaxNickname,
|
|
||||||
} from '../../util/Username';
|
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { sleep } from '../../util/sleep';
|
import { sleep } from '../../util/sleep';
|
||||||
import { assertDev } from '../../util/assert';
|
import { assertDev } from '../../util/assert';
|
||||||
|
@ -166,17 +161,6 @@ export function reserveUsername(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidNickname(nickname)) {
|
|
||||||
const error = getNicknameInvalidError(nickname);
|
|
||||||
if (error) {
|
|
||||||
dispatch(setUsernameReservationError(error));
|
|
||||||
} else {
|
|
||||||
assertDev(false, 'This should not happen');
|
|
||||||
dispatch(setUsernameReservationError(UsernameReservationError.General));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username } = getMe(getState());
|
const { username } = getMe(getState());
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
@ -364,6 +348,14 @@ export function reducer(
|
||||||
stateError = UsernameReservationError.CheckCharacters;
|
stateError = UsernameReservationError.CheckCharacters;
|
||||||
} else if (error === ReserveUsernameError.Conflict) {
|
} else if (error === ReserveUsernameError.Conflict) {
|
||||||
stateError = UsernameReservationError.UsernameNotAvailable;
|
stateError = UsernameReservationError.UsernameNotAvailable;
|
||||||
|
} else if (error === ReserveUsernameError.NotEnoughCharacters) {
|
||||||
|
stateError = UsernameReservationError.NotEnoughCharacters;
|
||||||
|
} else if (error === ReserveUsernameError.TooManyCharacters) {
|
||||||
|
stateError = UsernameReservationError.TooManyCharacters;
|
||||||
|
} else if (error === ReserveUsernameError.CheckStartingCharacter) {
|
||||||
|
stateError = UsernameReservationError.CheckStartingCharacter;
|
||||||
|
} else if (error === ReserveUsernameError.CheckCharacters) {
|
||||||
|
stateError = UsernameReservationError.CheckCharacters;
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(error);
|
throw missingCaseError(error);
|
||||||
}
|
}
|
||||||
|
@ -485,30 +477,3 @@ export function reducer(
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
function getNicknameInvalidError(
|
|
||||||
nickname: string | undefined
|
|
||||||
): UsernameReservationError | undefined {
|
|
||||||
if (!nickname) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nickname.length < getMinNickname()) {
|
|
||||||
return UsernameReservationError.NotEnoughCharacters;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[0-9a-z_]+$/.test(nickname)) {
|
|
||||||
return UsernameReservationError.CheckCharacters;
|
|
||||||
}
|
|
||||||
if (!/^[a-z_]/.test(nickname)) {
|
|
||||||
return UsernameReservationError.CheckStartingCharacter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nickname.length > getMaxNickname()) {
|
|
||||||
return UsernameReservationError.TooManyCharacters;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
|
@ -131,36 +131,6 @@ describe('electron/state/ducks/username', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const NICKNAME_ERROR_COMBOS = [
|
|
||||||
['x', UsernameReservationError.NotEnoughCharacters],
|
|
||||||
['x'.repeat(128), UsernameReservationError.TooManyCharacters],
|
|
||||||
['#$&^$)(', UsernameReservationError.CheckCharacters],
|
|
||||||
['1abcdefg', UsernameReservationError.CheckStartingCharacter],
|
|
||||||
];
|
|
||||||
for (const [nickname, error] of NICKNAME_ERROR_COMBOS) {
|
|
||||||
// eslint-disable-next-line no-loop-func
|
|
||||||
it(`should dispatch ${error} error for "${nickname}"`, async () => {
|
|
||||||
const clock = sandbox.useFakeTimers();
|
|
||||||
|
|
||||||
const doReserveUsername = sinon.stub().resolves(DEFAULT_RESERVATION);
|
|
||||||
const dispatch = sinon.spy();
|
|
||||||
|
|
||||||
actions.reserveUsername(nickname, {
|
|
||||||
doReserveUsername,
|
|
||||||
})(dispatch, () => emptyState, null);
|
|
||||||
|
|
||||||
await clock.runToLastAsync();
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(dispatch);
|
|
||||||
sinon.assert.calledWith(dispatch, {
|
|
||||||
type: 'username/SET_RESERVATION_ERROR',
|
|
||||||
payload: {
|
|
||||||
error,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should update reservation on success', () => {
|
it('should update reservation on success', () => {
|
||||||
let state = emptyState;
|
let state = emptyState;
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
import * as Username from '../../util/Username';
|
|
||||||
|
|
||||||
describe('Username', () => {
|
|
||||||
describe('isValidUsername', () => {
|
|
||||||
const { isValidUsername } = Username;
|
|
||||||
|
|
||||||
it('does not match invalid username searches', () => {
|
|
||||||
assert.isFalse(isValidUsername('username!'));
|
|
||||||
assert.isFalse(isValidUsername('1username'));
|
|
||||||
assert.isFalse(isValidUsername('u'));
|
|
||||||
assert.isFalse(isValidUsername('username9012345678901234567890123'));
|
|
||||||
assert.isFalse(isValidUsername('username.abc'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches valid usernames', () => {
|
|
||||||
assert.isTrue(isValidUsername('username_34'));
|
|
||||||
assert.isTrue(isValidUsername('u5ername'));
|
|
||||||
assert.isTrue(isValidUsername('_username'));
|
|
||||||
assert.isTrue(isValidUsername('use'));
|
|
||||||
assert.isTrue(isValidUsername('username901234567890123456789012'));
|
|
||||||
assert.isTrue(isValidUsername('username.0123'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not match valid and invalid usernames with @ prefix or suffix', () => {
|
|
||||||
assert.isFalse(isValidUsername('@username_34'));
|
|
||||||
assert.isFalse(isValidUsername('@1username'));
|
|
||||||
assert.isFalse(isValidUsername('username_34@'));
|
|
||||||
assert.isFalse(isValidUsername('1username@'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -10,6 +10,12 @@ export type UsernameReservationType = Readonly<{
|
||||||
export enum ReserveUsernameError {
|
export enum ReserveUsernameError {
|
||||||
Unprocessable = 'Unprocessable',
|
Unprocessable = 'Unprocessable',
|
||||||
Conflict = 'Conflict',
|
Conflict = 'Conflict',
|
||||||
|
|
||||||
|
// Maps to UsernameReservationError in state/ducks/usernameEnums.ts
|
||||||
|
NotEnoughCharacters = 'NotEnoughCharacters',
|
||||||
|
TooManyCharacters = 'TooManyCharacters',
|
||||||
|
CheckStartingCharacter = 'CheckStartingCharacter',
|
||||||
|
CheckCharacters = 'CheckCharacters',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConfirmUsernameResult {
|
export enum ConfirmUsernameResult {
|
||||||
|
|
|
@ -13,29 +13,3 @@ export function getMaxNickname(): number {
|
||||||
export function getMinNickname(): number {
|
export function getMinNickname(): number {
|
||||||
return parseIntWithFallback(RemoteConfig.getValue('global.nicknames.min'), 3);
|
return parseIntWithFallback(RemoteConfig.getValue('global.nicknames.min'), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidNickname(nickname: string): boolean {
|
|
||||||
if (!/^[a-z_][0-9a-z_]*$/i.test(nickname)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nickname.length < getMinNickname()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nickname.length > getMaxNickname()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidUsername(username: string): boolean {
|
|
||||||
const match = username.match(/^([a-z_][0-9a-z_]*)(\.\d+)?$/i);
|
|
||||||
if (!match) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, nickname] = match;
|
|
||||||
return isValidNickname(nickname);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { usernames } from '@signalapp/libsignal-client';
|
import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client';
|
||||||
|
|
||||||
import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername';
|
import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername';
|
||||||
import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
|
import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
|
||||||
|
@ -15,7 +15,6 @@ import { showToast } from './showToast';
|
||||||
import { strictAssert } from './assert';
|
import { strictAssert } from './assert';
|
||||||
import type { UUIDFetchStateKeyType } from './uuidFetchState';
|
import type { UUIDFetchStateKeyType } from './uuidFetchState';
|
||||||
import { getUuidsForE164s } from './getUuidsForE164s';
|
import { getUuidsForE164s } from './getUuidsForE164s';
|
||||||
import { isValidUsername } from './Username';
|
|
||||||
|
|
||||||
export type LookupConversationWithoutUuidActionsType = Readonly<{
|
export type LookupConversationWithoutUuidActionsType = Readonly<{
|
||||||
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
|
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
|
||||||
|
@ -137,10 +136,6 @@ export async function lookupConversationWithoutUuid(
|
||||||
async function checkForUsername(
|
async function checkForUsername(
|
||||||
username: string
|
username: string
|
||||||
): Promise<FoundUsernameType | undefined> {
|
): Promise<FoundUsernameType | undefined> {
|
||||||
if (!isValidUsername(username)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { server } = window.textsecure;
|
const { server } = window.textsecure;
|
||||||
if (!server) {
|
if (!server) {
|
||||||
throw new Error('server is not available!');
|
throw new Error('server is not available!');
|
||||||
|
@ -161,11 +156,15 @@ async function checkForUsername(
|
||||||
username,
|
username,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof HTTPError)) {
|
if (error instanceof HTTPError) {
|
||||||
throw error;
|
if (error.code === 404) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.code === 404) {
|
// Invalid username
|
||||||
|
if (error instanceof LibSignalErrorBase) {
|
||||||
|
log.error('checkForUsername: invalid username');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue