Close remaining 10-vs-11-digit gaps in PSTN number input
Full audit of every phone-number comparison/storage point across pstn-trunk.sh, security-dashboard.sh, asterisk.sh, asterisk-digital-ocean.sh, and the vendored easy-asterisk base script, prompted by the inbound Caller-ID normalization fix — the same digit-count mismatch was also possible on the admin-input side, just silent instead of loud: - security-dashboard.sh's write_permission(): a 10-digit whitelist entry was silently DROPPED (NUMBER_RE required exactly 11 digits), with no warning unless every entry in the field was invalid — a mixed 10-digit + 11-digit list saved "successfully" while quietly losing the 10-digit one. Now normalizes any bare 10-digit token to 11-digit instead of discarding it (_normalize_nanp_number). - write_personal_did(): required exactly 10 digits, rejecting an 11-digit entry outright with a clear (but avoidable) error. Now accepts either and normalizes to the canonical 10-digit storage form (_normalize_personal_did_input). - pstn-trunk.sh's TRUNK_DID install prompt: same fix, strips a leading "1" instead of aborting the install over it. Everything else checked out clean: outbound dialed-number matching already normalizes via the existing _NXXNXXXXXX pattern (adds "1" before any tier check), the area-code/country-code international gates aren't phone numbers so digit-count doesn't apply, and asterisk.sh/ asterisk-digital-ocean.sh/the vendored base script have no phone-number comparison logic at all — this class of bug only lives in the PSTN permission/whitelist layer this project added on top.
This commit is contained in:
@@ -1666,6 +1666,13 @@ install_pstn-trunk() {
|
|||||||
|
|
||||||
local TRUNK_DID=""
|
local TRUNK_DID=""
|
||||||
prompt_text "DID (the 10-digit US phone number assigned to this trunk, digits only):" "" TRUNK_DID
|
prompt_text "DID (the 10-digit US phone number assigned to this trunk, digits only):" "" TRUNK_DID
|
||||||
|
# Accept an 11-digit entry (leading "1") too and strip it — every other
|
||||||
|
# DID/number field in this system (personal_did, allowed_numbers) has
|
||||||
|
# the same 10-vs-11-digit ambiguity, and rejecting a plainly-valid
|
||||||
|
# 11-digit number here just to force a re-prompt is needless friction.
|
||||||
|
if [[ "$TRUNK_DID" =~ ^1([0-9]{10})$ ]]; then
|
||||||
|
TRUNK_DID="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
if [[ ! "$TRUNK_DID" =~ ^[0-9]{10}$ ]]; then
|
if [[ ! "$TRUNK_DID" =~ ^[0-9]{10}$ ]]; then
|
||||||
log_error "That doesn't look like a 10-digit US number — aborting."
|
log_error "That doesn't look like a 10-digit US number — aborting."
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -782,6 +782,23 @@ EXT_HEADER_RE = re.compile(r"^\[(\d+)\]")
|
|||||||
EXTEN_RE = re.compile(r"^\d+$")
|
EXTEN_RE = re.compile(r"^\d+$")
|
||||||
TIER_RE = re.compile(r"^(internal|restricted|full)$")
|
TIER_RE = re.compile(r"^(internal|restricted|full)$")
|
||||||
NUMBER_RE = re.compile(r"^\d{11}$")
|
NUMBER_RE = re.compile(r"^\d{11}$")
|
||||||
|
NUMBER_RE_10 = re.compile(r"^\d{10}$")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_nanp_number(token):
|
||||||
|
"""Accepts either a bare 10-digit NANP number (the natural way to type
|
||||||
|
a US number) or an already-11-digit one (leading "1" country code) and
|
||||||
|
returns the canonical 11-digit form allowed_numbers is always stored
|
||||||
|
in - REGEX() comparisons against CALLERID(num) require an exact
|
||||||
|
digit-count match, and this used to silently DROP a plain 10-digit
|
||||||
|
entry instead of normalizing it, the admin-input-side twin of the bug
|
||||||
|
that was also failing inbound calls whose Caller-ID itself arrived
|
||||||
|
without a leading "1" (see PSTN_CALLERID_NORM in pstn-trunk.sh)."""
|
||||||
|
if NUMBER_RE_10.match(token):
|
||||||
|
return "1" + token
|
||||||
|
if NUMBER_RE.match(token):
|
||||||
|
return token
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
SECURITY_LOG_TAIL_BYTES = 2 * 1024 * 1024 # comfortably enough for 5000 lines
|
SECURITY_LOG_TAIL_BYTES = 2 * 1024 * 1024 # comfortably enough for 5000 lines
|
||||||
@@ -1149,11 +1166,13 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False):
|
|||||||
the calling tier (see pstn-trunk.sh's file-level comment: an extension
|
the calling tier (see pstn-trunk.sh's file-level comment: an extension
|
||||||
can be internal-tier for calling and still messaging-enabled, or vice
|
can be internal-tier for calling and still messaging-enabled, or vice
|
||||||
versa), so it's set/cleared regardless of which tier branch runs below.
|
versa), so it's set/cleared regardless of which tier branch runs below.
|
||||||
Numbers are normalized to a pipe-separated list of 11-digit US numbers —
|
Numbers are normalized to a pipe-separated list of 11-digit US numbers
|
||||||
pipe, not comma, because the dialplan uses this value directly as a
|
(a bare 10-digit entry gets a leading "1" added, not dropped — see
|
||||||
REGEX() alternation pattern (see services/pstn-trunk.sh's file-level
|
_normalize_nanp_number) — pipe, not comma, because the dialplan uses
|
||||||
comment on why the untrusted call data is always the string being
|
this value directly as a REGEX() alternation pattern (see
|
||||||
tested, never interpolated into the pattern side)."""
|
services/pstn-trunk.sh's file-level comment on why the untrusted call
|
||||||
|
data is always the string being tested, never interpolated into the
|
||||||
|
pattern side)."""
|
||||||
if not ASTERISK_CONFIG_DIR:
|
if not ASTERISK_CONFIG_DIR:
|
||||||
return False, "No Asterisk install detected on this box"
|
return False, "No Asterisk install detected on this box"
|
||||||
ext = str(ext).strip()
|
ext = str(ext).strip()
|
||||||
@@ -1163,7 +1182,7 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False):
|
|||||||
return False, "Invalid tier"
|
return False, "Invalid tier"
|
||||||
|
|
||||||
tokens = re.split(r"[,\s|]+", (numbers_raw or "").strip())
|
tokens = re.split(r"[,\s|]+", (numbers_raw or "").strip())
|
||||||
clean_numbers = [t for t in tokens if NUMBER_RE.match(t)]
|
clean_numbers = [n for n in (_normalize_nanp_number(t) for t in tokens if t) if n]
|
||||||
numbers = "|".join(clean_numbers)
|
numbers = "|".join(clean_numbers)
|
||||||
|
|
||||||
cp = _read_permissions_cp()
|
cp = _read_permissions_cp()
|
||||||
@@ -1405,6 +1424,19 @@ def write_limits(max_outbound, max_inbound):
|
|||||||
|
|
||||||
|
|
||||||
PERSONAL_DID_RE = re.compile(r"^\d{10}$")
|
PERSONAL_DID_RE = re.compile(r"^\d{10}$")
|
||||||
|
PERSONAL_DID_RE_11 = re.compile(r"^1\d{10}$")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_personal_did_input(did):
|
||||||
|
"""write_personal_did()'s DID field is hand-typed, same footgun as
|
||||||
|
allowed_numbers - accept either the canonical bare 10-digit form or an
|
||||||
|
11-digit one with the NANP "1" prefix, returning the canonical 10-digit
|
||||||
|
form either way instead of rejecting a plainly-valid 11-digit entry."""
|
||||||
|
if PERSONAL_DID_RE.match(did):
|
||||||
|
return did
|
||||||
|
if PERSONAL_DID_RE_11.match(did):
|
||||||
|
return did[1:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _personal_dids_path():
|
def _personal_dids_path():
|
||||||
@@ -1460,8 +1492,10 @@ def write_personal_did(did, owner):
|
|||||||
return False, "No Asterisk install detected on this box"
|
return False, "No Asterisk install detected on this box"
|
||||||
did = str(did).strip()
|
did = str(did).strip()
|
||||||
owner = str(owner).strip()
|
owner = str(owner).strip()
|
||||||
if not PERSONAL_DID_RE.match(did):
|
norm_did = _normalize_personal_did_input(did)
|
||||||
return False, "DID must be a 10-digit US number"
|
if norm_did is None:
|
||||||
|
return False, "DID must be a 10-digit US number (11-digit with a leading 1 also accepted)"
|
||||||
|
did = norm_did
|
||||||
|
|
||||||
is_group = owner.startswith("@")
|
is_group = owner.startswith("@")
|
||||||
group_name = owner[1:] if is_group else ""
|
group_name = owner[1:] if is_group else ""
|
||||||
@@ -3154,7 +3188,7 @@ function renderPstnTable() {
|
|||||||
<option value="full" ${e.tier === "full" ? "selected" : ""}>full</option>
|
<option value="full" ${e.tier === "full" ? "selected" : ""}>full</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="text" class="pstn-numbers" value="${esc(e.allowed_numbers)}" placeholder="15551234567,15559876543" ${e.tier === "restricted" ? "" : "disabled"}></td>
|
<td><input type="text" class="pstn-numbers" value="${esc(e.allowed_numbers)}" placeholder="5551234567,5559876543 (leading 1 optional)" ${e.tier === "restricted" ? "" : "disabled"}></td>
|
||||||
<td style="text-align:center"><input type="checkbox" class="pstn-messaging" ${e.messaging ? "checked" : ""}></td>
|
<td style="text-align:center"><input type="checkbox" class="pstn-messaging" ${e.messaging ? "checked" : ""}></td>
|
||||||
<td><button class="action" onclick="savePstnPermission('${esc(e.ext)}')">Save</button></td>
|
<td><button class="action" onclick="savePstnPermission('${esc(e.ext)}')">Save</button></td>
|
||||||
</tr>`).join("");
|
</tr>`).join("");
|
||||||
|
|||||||
Reference in New Issue
Block a user