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
|
||||
// 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 { strictAssert } from '../util/assert';
|
||||
|
@ -102,6 +106,35 @@ export async function reserveUsername(
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,6 @@ import {
|
|||
} from '../../types/Username';
|
||||
import * as usernameServices from '../../services/username';
|
||||
import type { ReserveUsernameResultType } from '../../services/username';
|
||||
import {
|
||||
isValidNickname,
|
||||
getMinNickname,
|
||||
getMaxNickname,
|
||||
} from '../../util/Username';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import { assertDev } from '../../util/assert';
|
||||
|
@ -166,17 +161,6 @@ export function reserveUsername(
|
|||
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 abortController = new AbortController();
|
||||
|
@ -364,6 +348,14 @@ export function reducer(
|
|||
stateError = UsernameReservationError.CheckCharacters;
|
||||
} else if (error === ReserveUsernameError.Conflict) {
|
||||
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 {
|
||||
throw missingCaseError(error);
|
||||
}
|
||||
|
@ -485,30 +477,3 @@ export function reducer(
|
|||
|
||||
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', () => {
|
||||
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 {
|
||||
Unprocessable = 'Unprocessable',
|
||||
Conflict = 'Conflict',
|
||||
|
||||
// Maps to UsernameReservationError in state/ducks/usernameEnums.ts
|
||||
NotEnoughCharacters = 'NotEnoughCharacters',
|
||||
TooManyCharacters = 'TooManyCharacters',
|
||||
CheckStartingCharacter = 'CheckStartingCharacter',
|
||||
CheckCharacters = 'CheckCharacters',
|
||||
}
|
||||
|
||||
export enum ConfirmUsernameResult {
|
||||
|
|
|
@ -13,29 +13,3 @@ export function getMaxNickname(): number {
|
|||
export function getMinNickname(): number {
|
||||
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
|
||||
// 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 { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
|
||||
|
@ -15,7 +15,6 @@ import { showToast } from './showToast';
|
|||
import { strictAssert } from './assert';
|
||||
import type { UUIDFetchStateKeyType } from './uuidFetchState';
|
||||
import { getUuidsForE164s } from './getUuidsForE164s';
|
||||
import { isValidUsername } from './Username';
|
||||
|
||||
export type LookupConversationWithoutUuidActionsType = Readonly<{
|
||||
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
|
||||
|
@ -137,10 +136,6 @@ export async function lookupConversationWithoutUuid(
|
|||
async function checkForUsername(
|
||||
username: string
|
||||
): Promise<FoundUsernameType | undefined> {
|
||||
if (!isValidUsername(username)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { server } = window.textsecure;
|
||||
if (!server) {
|
||||
throw new Error('server is not available!');
|
||||
|
@ -161,11 +156,15 @@ async function checkForUsername(
|
|||
username,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof HTTPError)) {
|
||||
throw error;
|
||||
if (error instanceof HTTPError) {
|
||||
if (error.code === 404) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 404) {
|
||||
// Invalid username
|
||||
if (error instanceof LibSignalErrorBase) {
|
||||
log.error('checkForUsername: invalid username');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue