security: fix the review leftovers (owner, 08 §6)
- Asterisk doesn't load app_system, func_shell, app_disa, res_agi, app_externalivr. - Turning two-step sign-in on asks for the password again. - *97/*98: one voicemail login at a time per phone, 10 an hour (sp-sub-vmlimit). - /prov: an unknown MAC gets the same 401 as a known one. - The login limiter sweeps keys past every window. Also the v1 checklist with the owner's results from 2026-10-21.
This commit is contained in:
1 parent
3c8828eedb
commit
8d5475cfbf
32 files changed
+501
-61
No files matched your search
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
import { expect, it } from 'vitest';
|
||||
import {
|
||||
isLoginBlocked,
|
||||
loginLimiterSize,
|
||||
recordLoginFailure,
|
||||
resetLoginLimiter,
|
||||
} from './login-limiter.ts';
|
||||
|
||||
it('forgets emails tried long ago instead of growing until restart', () => {
|
||||
resetLoginLimiter();
|
||||
const t0 = 1_000_000_000;
|
||||
for (let i = 0; i < 100; i++) recordLoginFailure(`10.0.0.${i}`, `user${i}@x.test`, t0);
|
||||
expect(loginLimiterSize()).toBe(200);
|
||||
for (let i = 0; i < 5; i++) recordLoginFailure('10.9.9.9', 'kept@x.test', t0 + 61 * 60_000);
|
||||
expect(loginLimiterSize()).toBe(2);
|
||||
expect(isLoginBlocked('10.9.9.9', 'other@x.test', t0 + 61 * 60_000)).toBe(true);
|
||||
});
|
||||
@@ -3,8 +3,10 @@
|
||||
// ponytail: in-process memory, resets on restart; fine for one api process (v1 has no HA).
|
||||
const MIN = 60_000;
|
||||
const RULES = { ip: { max: 5, windowMs: 15 * MIN }, email: { max: 10, windowMs: 60 * MIN } };
|
||||
const LONGEST = Math.max(RULES.ip.windowMs, RULES.email.windowMs);
|
||||
|
||||
const failures = new Map<string, number[]>();
|
||||
let lastSweep = 0;
|
||||
|
||||
function recent(key: string, windowMs: number, now: number): number[] {
|
||||
const list = (failures.get(key) ?? []).filter((t) => now - t < windowMs);
|
||||
@@ -21,12 +23,24 @@ export function isLoginBlocked(ip: string, email: string, now = Date.now()): boo
|
||||
}
|
||||
|
||||
export function recordLoginFailure(ip: string, email: string, now = Date.now()): void {
|
||||
// Forget keys past every window, at most once a minute: emails tried once and never again
|
||||
// would otherwise stay until restart (08 §6).
|
||||
if (now - lastSweep > MIN) {
|
||||
lastSweep = now;
|
||||
for (const [key, list] of failures) if (now - list.at(-1)! >= LONGEST) failures.delete(key);
|
||||
}
|
||||
for (const key of [`ip:${ip}`, `email:${email.toLowerCase()}`]) {
|
||||
failures.set(key, [...(failures.get(key) ?? []), now]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Tests only. */
|
||||
export function loginLimiterSize(): number {
|
||||
return failures.size;
|
||||
}
|
||||
|
||||
/** Tests only. */
|
||||
export function resetLoginLimiter(): void {
|
||||
failures.clear();
|
||||
lastSweep = 0;
|
||||
}
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
+21
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
+21
@@ -109,14 +109,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=${SP_EXT})
|
||||
same => n,GotoIf($[${GROUP_COUNT(${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=${STRFTIME(${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=${DB(SP/VMLOGIN/${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=${IF($["${CUT(V,:,1)}" = "${H}"]?${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/${SP_EXT})=${H}:$[${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: ${SP_EXT},Value: 1)
|
||||
|
||||
@@ -112,14 +112,35 @@ exten => _X!,1,Answer() ; X! also matches "0" (caller unknown)
|
||||
same => n,SayDigits(\${EXTEN})
|
||||
same => n,Hangup()
|
||||
|
||||
; ─── Voicemail logins (*97, *98) from a phone: one at a time and 10 an hour, 3 PIN tries each
|
||||
; (voicemail.conf maxlogins), so a phone can't run through the PINs (08 §6). Returns "ok".
|
||||
[sp-sub-vmlimit]
|
||||
exten => s,1,Set(GROUP(spvm)=\${SP_EXT})
|
||||
same => n,GotoIf($[\${GROUP_COUNT(\${SP_EXT}@spvm)} > 1]?no)
|
||||
same => n,Set(LOCAL(H)=\${STRFTIME(\${EPOCH},UTC,%Y%m%d%H)})
|
||||
same => n,Set(LOCAL(V)=\${DB(SP/VMLOGIN/\${SP_EXT})})
|
||||
same => n,Set(LOCAL(N)=\${IF($["\${CUT(V,:,1)}" = "\${H}"]?\${CUT(V,:,2)}:0)})
|
||||
same => n,GotoIf($[\${N} >= 10]?no)
|
||||
same => n,Set(DB(SP/VMLOGIN/\${SP_EXT})=\${H}:$[\${N} + 1])
|
||||
same => n,Return(ok)
|
||||
same => n(no),Return()
|
||||
|
||||
; ─── Feature codes (03 §6.13).
|
||||
[sp-featurecodes]
|
||||
exten => *97,1,Answer() ; my voicemail
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["\${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(\${SP_EXT}@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *98,1,Answer() ; any voicemail (asks mailbox)
|
||||
same => n,Gosub(sp-sub-vmlimit,s,1)
|
||||
same => n,GotoIf($["\${GOSUB_RETVAL}" != "ok"]?full)
|
||||
same => n,VoiceMailMain(@default)
|
||||
same => n,Hangup()
|
||||
same => n(full),Playback(an-error-has-occurred)
|
||||
same => n,Hangup()
|
||||
exten => *78,1,Answer() ; DND on
|
||||
same => n,Set(DB(SP/DND/\${SP_EXT})=1)
|
||||
same => n,UserEvent(SpToggle,Key: DND,Ext: \${SP_EXT},Value: 1)
|
||||
|
||||
@@ -114,8 +114,11 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
|
||||
app.post('/api/auth/totp/enable', { preHandler: requireAuth }, async (req) => {
|
||||
adminOnly(req.user!);
|
||||
const { secret, code } = TotpEnableSchema.parse(req.body);
|
||||
if ((await myRow(req.user!.id)).totpSecretEnc) throw conflict('Two-step sign-in is already on');
|
||||
const { secret, code, password } = TotpEnableSchema.parse(req.body);
|
||||
const u = await myRow(req.user!.id);
|
||||
if (u.totpSecretEnc) throw conflict('Two-step sign-in is already on');
|
||||
// Like turning it off: a stolen session alone can't lock the owner out with its own secret.
|
||||
if (!(await verifyPassword(u.passwordHash, password))) throw badRequest('Wrong password');
|
||||
const step = checkTotp(secret, code, null);
|
||||
if (step === null)
|
||||
throw badRequest('That code doesn’t match. Check the phone’s clock and try again.');
|
||||
@@ -135,7 +138,7 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
return { recoveryCodes: codes };
|
||||
});
|
||||
|
||||
/** Turning it off or making new recovery codes needs the password again. */
|
||||
/** Turning it off or making new recovery codes needs the password again (on does too). */
|
||||
async function confirmPassword(id: string, body: unknown) {
|
||||
const { password } = PasswordConfirmSchema.parse(body);
|
||||
const u = await myRow(id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// GET /prov/... (07 §2): phones fetch their config with HTTP Basic auth (user = MAC, password =
|
||||
// the device's provisioning password). No session. Unknown MAC → 404, bad auth → 401; both are
|
||||
// logged with the IP and count toward an IP block (provisioning/limiter.ts).
|
||||
// the device's provisioning password). No session. Unknown MAC or bad auth → 401, alike; wrong
|
||||
// credentials are logged with the IP and count toward an IP block (provisioning/limiter.ts).
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
import { BlockList, isIPv4 } from 'node:net';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
@@ -113,24 +113,28 @@ export async function provisioningRoutes(app: FastifyInstance) {
|
||||
if (!kind) return deny(reply, 404, 'Not found');
|
||||
const mac = kind.m![1]!;
|
||||
|
||||
const found = await loadContext(db, app.config, mac);
|
||||
if (
|
||||
!found ||
|
||||
found.ctx.vendor === 'generic' ||
|
||||
(kind.f.vendor && found.ctx.vendor !== kind.f.vendor)
|
||||
) {
|
||||
recordProvFailure(req.ip);
|
||||
req.log.warn({ ip: req.ip, mac }, 'provisioning: unknown MAC');
|
||||
return deny(reply, 404, 'Not found');
|
||||
}
|
||||
const loaded = await loadContext(db, app.config, mac);
|
||||
const found =
|
||||
loaded &&
|
||||
loaded.ctx.vendor !== 'generic' &&
|
||||
(!kind.f.vendor || loaded.ctx.vendor === kind.f.vendor)
|
||||
? loaded
|
||||
: null;
|
||||
const auth = basicAuth(req);
|
||||
const ok =
|
||||
auth !== null && auth.user.toLowerCase() === mac && same(auth.pass, found.ctx.provPassword);
|
||||
found !== null &&
|
||||
auth !== null &&
|
||||
auth.user.toLowerCase() === mac &&
|
||||
same(auth.pass, found.ctx.provPassword);
|
||||
if (!ok) {
|
||||
// A phone asks without credentials first and retries after the 401: not a failure.
|
||||
// An unknown MAC gets the same 401 as a known one, so the answers don't tell which MACs
|
||||
// exist (08 §6). A phone asks without credentials first and retries: not a failure.
|
||||
if (auth) {
|
||||
recordProvFailure(req.ip);
|
||||
req.log.warn({ ip: req.ip, mac }, 'provisioning: wrong credentials');
|
||||
req.log.warn(
|
||||
{ ip: req.ip, mac },
|
||||
found ? 'provisioning: wrong credentials' : 'provisioning: unknown MAC',
|
||||
);
|
||||
}
|
||||
return reply
|
||||
.code(401)
|
||||
|
||||
@@ -285,6 +285,31 @@ async function main() {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// Voicemail logins: *97 above counted once; with 10 this hour, *98 doesn't reach a PIN prompt.
|
||||
const hour = new Date().toISOString().replace(/\D/g, '').slice(0, 10);
|
||||
const counted = await ami.command('database get SP/VMLOGIN 101');
|
||||
if (!counted.includes(`${hour}:1`)) problems.push(`*97 not counted: ${counted}`);
|
||||
await ami.command(`database put SP/VMLOGIN 101 ${hour}:10`);
|
||||
await ami.action({
|
||||
Action: 'Originate',
|
||||
Channel: 'Local/*98@sp-from-ext',
|
||||
Application: 'Wait',
|
||||
Data: '5',
|
||||
Variable: '__SP_EXT=101',
|
||||
Async: 'true',
|
||||
});
|
||||
let refused = false;
|
||||
for (let i = 0; i < 20 && !refused; i++) {
|
||||
await sleep(500);
|
||||
refused = /Playing 'an-error-has-occurred/.test(docker('logs', AST).out);
|
||||
}
|
||||
if (!refused) problems.push('*98 past the hourly voicemail login limit was not refused');
|
||||
else if (/VoiceMailMain\("[^"]*", "@default"\)/.test(docker('logs', AST).out))
|
||||
problems.push('*98 reached VoiceMailMain past the limit');
|
||||
else log('*98 past 10 logins an hour: refused');
|
||||
await ami.command('channel request hangup all');
|
||||
await waitIdle();
|
||||
|
||||
// eslint-disable-next-line no-control-regex -- strips Asterisk's ANSI colors
|
||||
const plainLog = docker('logs', AST).out.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
problems.push(
|
||||
|
||||
@@ -105,10 +105,25 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect((await prov('y000000000044.cfg')).body).toBe('#!version:1.0.0.1\n');
|
||||
});
|
||||
|
||||
it('unknown MAC and the other vendor’s file are 404; ten failures block the IP', async () => {
|
||||
expect((await prov('001565000000.cfg')).statusCode).toBe(404);
|
||||
expect((await prov(`cfg${device.mac}.xml`)).statusCode).toBe(404); // a Yealink here
|
||||
it('unknown MAC and the other vendor’s file answer like a known MAC; ten failures block the IP', async () => {
|
||||
// 08 §6: the answers mustn't tell which MACs exist.
|
||||
const known = await prov(`${device.mac}.cfg`);
|
||||
for (const f of ['001565000000.cfg', `cfg${device.mac}.xml`]) {
|
||||
const r = await prov(f);
|
||||
expect([r.statusCode, r.headers['www-authenticate'], r.body]).toEqual([
|
||||
401,
|
||||
known.headers['www-authenticate'],
|
||||
known.body,
|
||||
]);
|
||||
}
|
||||
expect((await prov('anything.boot')).statusCode).toBe(404);
|
||||
// with credentials they're failures, like a wrong password
|
||||
expect(
|
||||
(await prov('001565000000.cfg', '192.168.1.20', 'guess', '001565000000')).statusCode,
|
||||
).toBe(401);
|
||||
expect(
|
||||
(await prov(`cfg${device.mac}.xml`, '192.168.1.20', device.provisioningPassword)).statusCode,
|
||||
).toBe(401); // a Yealink here
|
||||
for (let i = 0; i < 8; i++)
|
||||
expect((await prov(`${device.mac}.cfg`, '192.168.1.20', 'guess')).statusCode).toBe(401);
|
||||
// 10 failures (2 unknown MACs, 8 wrong passwords): even the right password is refused now
|
||||
@@ -181,7 +196,7 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect(
|
||||
(await prov('112233445566.cfg', '192.168.1.40', generic.provisioningPassword, '112233445566'))
|
||||
.statusCode,
|
||||
).toBe(404);
|
||||
).toBe(401);
|
||||
});
|
||||
|
||||
it('M10-T2: BLF keys named after their users, the time zone, and the phonebook', async () => {
|
||||
@@ -256,6 +271,6 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect((await req('DELETE', `/api/devices/${device.id}`)).statusCode).toBe(200);
|
||||
expect(
|
||||
(await prov(`${device.mac}.cfg`, '192.168.1.50', device.provisioningPassword)).statusCode,
|
||||
).toBe(404);
|
||||
).toBe(401);
|
||||
});
|
||||
});
|
||||
@@ -44,12 +44,18 @@ describe.skipIf(!hasTestDb)('two-step sign-in', () => {
|
||||
secret = setup.secret;
|
||||
expect(setup.otpauthUrl).toMatch(/^otpauth:\/\/totp\/.+\?secret=[A-Z2-7]{32}&issuer=/);
|
||||
expect(setup.qr).toMatch(/^data:image\/png;base64,/);
|
||||
const bad = await req('POST', '/api/auth/totp/enable', { secret, code: '000000' });
|
||||
const password = ADMIN.password;
|
||||
const bad = await req('POST', '/api/auth/totp/enable', { secret, code: '000000', password });
|
||||
expect(bad.statusCode).toBe(400);
|
||||
const ok = await req('POST', '/api/auth/totp/enable', {
|
||||
secret,
|
||||
code: totpCode(secret, stepAt()),
|
||||
});
|
||||
// A session alone isn't enough: the password again, like turning it off.
|
||||
const code = totpCode(secret, stepAt());
|
||||
for (const payload of [
|
||||
{ secret, code },
|
||||
{ secret, code, password: 'not-it' },
|
||||
])
|
||||
expect((await req('POST', '/api/auth/totp/enable', payload)).statusCode).toBe(400);
|
||||
expect((await req('GET', '/api/auth/totp')).json().enabled).toBe(false);
|
||||
const ok = await req('POST', '/api/auth/totp/enable', { secret, code, password });
|
||||
expect(ok.statusCode, ok.body).toBe(200);
|
||||
recovery = ok.json().recoveryCodes;
|
||||
expect(recovery).toHaveLength(10);
|
||||
@@ -58,7 +64,7 @@ describe.skipIf(!hasTestDb)('two-step sign-in', () => {
|
||||
recoveryCodesLeft: 10,
|
||||
});
|
||||
expect(
|
||||
(await req('POST', '/api/auth/totp/enable', { secret, code: '123456' })).statusCode,
|
||||
(await req('POST', '/api/auth/totp/enable', { secret, code: '123456', password })).statusCode,
|
||||
).toBe(409);
|
||||
});
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ export const authApi = {
|
||||
}).then((r) => r.user),
|
||||
totp: () => api<TotpStatus>('GET', '/api/auth/totp'),
|
||||
totpSetup: () => api<TotpSetup>('POST', '/api/auth/totp/setup', {}),
|
||||
totpEnable: (secret: string, code: string) =>
|
||||
api<{ recoveryCodes: string[] }>('POST', '/api/auth/totp/enable', { secret, code }),
|
||||
totpEnable: (secret: string, code: string, password: string) =>
|
||||
api<{ recoveryCodes: string[] }>('POST', '/api/auth/totp/enable', { secret, code, password }),
|
||||
totpDisable: (password: string) =>
|
||||
api<{ ok: true }>('POST', '/api/auth/totp/disable', { password }),
|
||||
totpNewCodes: (password: string) =>
|
||||
|
||||
@@ -30,11 +30,12 @@ function TwoStep() {
|
||||
const [password, setPassword] = useState('');
|
||||
const setup = useMutation({ mutationFn: authApi.totpSetup });
|
||||
const enable = useMutation({
|
||||
mutationFn: () => authApi.totpEnable(setup.data!.secret, code.trim()),
|
||||
mutationFn: () => authApi.totpEnable(setup.data!.secret, code.trim(), password),
|
||||
onSuccess: async (r) => {
|
||||
setCodes(r.recoveryCodes);
|
||||
setup.reset();
|
||||
setCode('');
|
||||
setPassword('');
|
||||
await refresh();
|
||||
},
|
||||
});
|
||||
@@ -131,8 +132,9 @@ function TwoStep() {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{passwordField}
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" variant="primary" disabled={enable.isPending}>
|
||||
<Button type="submit" variant="primary" disabled={!password || enable.isPending}>
|
||||
Turn on
|
||||
</Button>
|
||||
<Button onClick={() => setup.reset()}>Cancel</Button>
|
||||
|
||||
Reference in new issue
Block a user