Merge pull request #320 from outis1one/claude/extensions-qr-code-popup-6jlkcp
security-dashboard: put TURN creds in the Sipnetic QR, widen the popup
This commit is contained in:
@@ -3036,11 +3036,23 @@ def ea_device_sipnetic_string(extension):
|
|||||||
"""Sipnetic's own documented "account string" QR-scan format
|
"""Sipnetic's own documented "account string" QR-scan format
|
||||||
(https://www.sipnetic.com/qr-codes): semicolon-separated key=value pairs,
|
(https://www.sipnetic.com/qr-codes): semicolon-separated key=value pairs,
|
||||||
n=display name, u=username, d=domain/IP (no port), p=password,
|
n=display name, u=username, d=domain/IP (no port), p=password,
|
||||||
dt=default transport (0=UDP, 1=TCP, 2=TLS). Unlike
|
dt=default transport (0=UDP, 1=TCP, 2=TLS), st=STUN/TURN server. Unlike
|
||||||
ea_device_provisioning()'s deliberately generic plain-text file, this one
|
ea_device_provisioning()'s deliberately generic plain-text file, this one
|
||||||
IS a verified, documented format for one specific app, built from the
|
IS a verified, documented format for one specific app, built from the
|
||||||
exact same ea_device_details() data.
|
exact same ea_device_details() data.
|
||||||
|
|
||||||
|
st is intentionally set to an explicit turn:user:pass@host:port URI
|
||||||
|
(Sipnetic's doc confirms the st field accepts embedded credentials in
|
||||||
|
that form) whenever coturn is configured, rather than left unset --
|
||||||
|
leaving it out doesn't mean "no TURN", it means Sipnetic falls back to
|
||||||
|
its own default/built-in STUN server instead of the coturn instance
|
||||||
|
Asterisk itself is actually using, which is silently wrong rather than
|
||||||
|
absent. This is the same TURN_SERVER/TURN_USERNAME/TURN_PASSWORD
|
||||||
|
ea_device_details() already reads from the Asterisk .env (see
|
||||||
|
ea_connection_defaults()) -- whichever coturn Asterisk was configured
|
||||||
|
against (the shared instance on a droplet, or an embedded one), not a
|
||||||
|
second, separately-derived value.
|
||||||
|
|
||||||
A literal ';' in any field must be doubled per that same doc page --
|
A literal ';' in any field must be doubled per that same doc page --
|
||||||
generated passwords are alnum-only (_ea_generate_password) so this only
|
generated passwords are alnum-only (_ea_generate_password) so this only
|
||||||
matters for a hand-typed device name, but escaping costs nothing."""
|
matters for a hand-typed device name, but escaping costs nothing."""
|
||||||
@@ -3053,10 +3065,18 @@ def ea_device_sipnetic_string(extension):
|
|||||||
|
|
||||||
host = d["server"] or ""
|
host = d["server"] or ""
|
||||||
dt = "2" if d["transport"] == "TLS" else "0"
|
dt = "2" if d["transport"] == "TLS" else "0"
|
||||||
return "n=%s;u=%s;d=%s;p=%s;dt=%s;" % (
|
fields = [
|
||||||
esc_field(d["name"] or d["extension"]), esc_field(d["extension"]),
|
"n=%s" % esc_field(d["name"] or d["extension"]),
|
||||||
esc_field(host), esc_field(d["password"]), dt,
|
"u=%s" % esc_field(d["extension"]),
|
||||||
)
|
"d=%s" % esc_field(host),
|
||||||
|
"p=%s" % esc_field(d["password"]),
|
||||||
|
"dt=%s" % dt,
|
||||||
|
]
|
||||||
|
if d.get("turn_server") and d.get("turn_username") and d.get("turn_password"):
|
||||||
|
fields.append("st=%s" % esc_field(
|
||||||
|
"turn:%s:%s@%s" % (d["turn_username"], d["turn_password"], d["turn_server"])
|
||||||
|
))
|
||||||
|
return ";".join(fields) + ";"
|
||||||
|
|
||||||
|
|
||||||
def _ea_edit_device_block(extension, mutate):
|
def _ea_edit_device_block(extension, mutate):
|
||||||
@@ -3814,8 +3834,12 @@ INDEX_HTML = """<!doctype html>
|
|||||||
padding: var(--sp-4);
|
padding: var(--sp-4);
|
||||||
}
|
}
|
||||||
#qr-modal-overlay.show { display: flex; }
|
#qr-modal-overlay.show { display: flex; }
|
||||||
|
/* Sized to the QR frame's own 192px, not this -- the caption text below it
|
||||||
|
(TURN credentials warning included) wrapped down to a couple of
|
||||||
|
characters per line at that width. 320px gives it room to breathe while
|
||||||
|
staying well short of the surrounding card. */
|
||||||
.qr-modal {
|
.qr-modal {
|
||||||
position: relative;
|
position: relative; width: 320px; max-width: calc(100vw - 2 * var(--sp-4));
|
||||||
background: var(--surface); border: 1px solid var(--line);
|
background: var(--surface); border: 1px solid var(--line);
|
||||||
border-radius: var(--radius); padding: var(--sp-4);
|
border-radius: var(--radius); padding: var(--sp-4);
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
|
||||||
@@ -4148,7 +4172,8 @@ INDEX_HTML = """<!doctype html>
|
|||||||
<div class="qr-modal">
|
<div class="qr-modal">
|
||||||
<button class="qr-modal-close" onclick="closeSipneticQr()" aria-label="Close">×</button>
|
<button class="qr-modal-close" onclick="closeSipneticQr()" aria-label="Close">×</button>
|
||||||
<div id="qr-modal-canvas"></div>
|
<div id="qr-modal-canvas"></div>
|
||||||
<p class="muted" style="margin:0; font-size:0.8rem; text-align:center">Scan with Sipnetic (Add Account → Scan QR Code)</p>
|
<p class="muted" style="margin:var(--sp-2) 0 0; font-size:0.8rem; width:100%">Scan with Sipnetic (Add Account → Scan QR Code) to auto-fill this extension's SIP and TURN settings.</p>
|
||||||
|
<p class="muted" style="margin:0; font-size:0.75rem; width:100%">Contains the extension's password and TURN credentials in plain text — treat the image like the password itself.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -5600,9 +5625,18 @@ async function showSipneticQr(ext) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
canvas.innerHTML = "";
|
canvas.innerHTML = "";
|
||||||
new QRCode(canvas, {
|
new QRCode(canvas, {
|
||||||
// 152 = the 192px frame's content box after its 20px white quiet-zone
|
// Rendered at 3x the on-screen size (456, not 152) then scaled back down
|
||||||
// padding on each side (192 - 2*20) -- keep in sync with #qr-modal-canvas.
|
// by the #qr-modal-canvas img/canvas{width:100%} CSS rule -- the physical
|
||||||
text: data.account_string, width: 152, height: 152,
|
// footprint doesn't change, but the extra resolution gives the browser's
|
||||||
|
// downscaling real anti-aliasing to work with instead of the blocky
|
||||||
|
// 1px-per-module edges qrcode.js draws natively. Verified with a headless
|
||||||
|
// render + OpenCV decode: since the st= TURN field was added, the account
|
||||||
|
// string is long enough to need a denser QR (more modules in the same
|
||||||
|
// frame) that a real camera reads far more reliably at this resolution
|
||||||
|
// than at 1:1. 152 = the 192px frame's content box after its 20px white
|
||||||
|
// quiet-zone padding on each side (192 - 2*20) -- keep the *displayed*
|
||||||
|
// size (via CSS, not this) in sync with #qr-modal-canvas if that changes.
|
||||||
|
text: data.account_string, width: 456, height: 456,
|
||||||
correctLevel: QRCode.CorrectLevel.M,
|
correctLevel: QRCode.CorrectLevel.M,
|
||||||
});
|
});
|
||||||
overlay.classList.add("show");
|
overlay.classList.add("show");
|
||||||
|
|||||||
Reference in New Issue
Block a user