mkdir -p /root/switch
cat > /root/switch/install-switch.sh <<'OUTER_EOF'
OUTER_EOF

tail -5 /root/switch/install-switch.sh
nano /root/switch/install-switch.sh
chmod +x /root/switch/install-switch.sh
root/switch/install-switch.sh
la -la /root/switch/
mkdir -p /root/switch
cd /root/switch
cat > install-switch.sh <<'Outer_EOF'
#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail

# ============================== FILL THESE IN ==============================
DOMAIN="sip.YOURDOMAIN.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="you@example.com"              # Let's Encrypt contact
SW_TOKEN="swpat_xxxxxxxxxxxx"        # SignalWire Personal Access Token (free)

TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="your-trunk-user"
TRUNK_PASS="your-trunk-pass"

EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"

FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================

log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"

PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"

export DEBIAN_FRONTEND=noninteractive

log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null

log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ $(lsb_release -sc) main" \
  > /etc/apt/sources.list.d/freeswitch.list
apt-get update -qq
apt-get install -y -qq freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-sounds-music >/dev/null
[[ -d /etc/freeswitch ]] || cp -a /usr/share/freeswitch/conf/vanilla/. /etc/freeswitch/

log "requesting a TLS certificate for $DOMAIN"
systemctl stop freeswitch 2>/dev/null || true
certbot certonly --standalone --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN"
mkdir -p /etc/freeswitch/tls
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
  > /etc/freeswitch/tls/wss.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/tls.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 640 /etc/freeswitch/tls/*.pem

log "setting domain and external IP"
sed -i -E "s#(data=\"domain=)[^\"]*#\1${DOMAIN}#"            /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_rtp_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_sip_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml

log "writing the webrtc SIP profile (WSS on 7443)"
cat > /etc/freeswitch/sip_profiles/webrtc.xml <<'EOF'
<profile name="webrtc">
  <settings>
    <param name="context" value="default"/>
    <param name="dialplan" value="XML"/>
    <param name="sip-port" value="5061"/>
    <param name="tls" value="true"/>
    <param name="tls-only" value="true"/>
    <param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
    <param name="wss-binding" value=":7443"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-secure-media" value="mandatory:AEAD_AES_256_GCM_8"/>
    <param name="apply-candidate-acl" value="wan_v4.auto"/>
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>
    <param name="auth-calls" value="true"/>
    <param name="challenge-realm" value="auto_from"/>
    <param name="accept-blind-reg" value="false"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
  </settings>
</profile>
EOF

log "creating line $EXT_USER with caller ID $FAKE_CID"
cat > /etc/freeswitch/directory/default/${EXT_USER}.xml <<'EOF'
<include>
  <user id="__EXT_USER__">
    <params>
      <param name="password" value="__EXT_PASS__"/>
      <param name="max-registrations" value="2"/>
    </params>
    <variables>
      <variable name="effective_caller_id_number" value="__FAKE_CID__"/>
      <variable name="effective_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="outbound_caller_id_number" value="__FAKE_CID__"/>
      <variable name="outbound_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="user_context" value="default"/>
    </variables>
  </user>
</include>
EOF
sed -i "s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g; \
        s/__FAKE_CID_NAME__/${FAKE_CID_NAME}/g; s/__FAKE_CID__/${FAKE_CID}/g" \
  /etc/freeswitch/directory/default/${EXT_USER}.xml

log "writing the outbound dialplan"
cat > /etc/freeswitch/dialplan/default/00_outbound.xml <<'EOF'
<include>
  <extension name="outbound_trunk">
    <condition field="destination_number" expression="^(\+?[0-9]{7,15})$">
      <action application="set" data="hangup_after_bridge=true"/>
      <action application="set" data="continue_on_fail=true"/>
      <action application="bridge" data="sofia/gateway/trunk/$1"/>
    </condition>
  </extension>
</include>
EOF

log "writing the trunk gateway"
cat > /etc/freeswitch/sip_profiles/external/trunk.xml <<'EOF'
<include>
  <gateway name="trunk">
    <param name="proxy" value="__TRUNK_HOST__"/>
    <param name="realm" value="__TRUNK_HOST__"/>
    <param name="username" value="__TRUNK_USER__"/>
    <param name="password" value="__TRUNK_PASS__"/>
    <param name="from-domain" value="__TRUNK_HOST__"/>
    <param name="register" value="true"/>
    <param name="caller-id-in-from" value="true"/>
    <param name="retry-seconds" value="30"/>
  </gateway>
</include>
EOF
sed -i "s/__TRUNK_HOST__/${TRUNK_HOST}/g; s/__TRUNK_USER__/${TRUNK_USER}/g; \
        s/__TRUNK_PASS__/${TRUNK_PASS}/g" /etc/freeswitch/sip_profiles/external/trunk.xml

log "opening the firewall"
ufw allow 22/tcp    >/dev/null
ufw allow 80/tcp    >/dev/null
ufw allow 5060/udp  >/dev/null
ufw allow 5060/tcp  >/dev/null
ufw allow 5061/tcp  >/dev/null
ufw allow 7443/tcp  >/dev/null
ufw allow 16384:32768/udp >/dev/null
ufw --force enable  >/dev/null

log "starting freeswitch"
systemctl enable --now freeswitch
sleep 6

cat > /root/sip-test.html <<'EOF'
<!doctype html><html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:0 16px}
input{width:100%;padding:8px;margin:4px 0;box-sizing:border-box}
button{padding:10px 18px;margin:6px 6px 6px 0;cursor:pointer}
pre{background:#f4f4f4;padding:12px;height:240px;overflow:auto;font-size:12px}</style></head>
<body><h2>WSS registration test</h2>
<input id="wss"   value="wss://__DOMAIN__:7443">
<input id="user"  value="__EXT_USER__">
<input id="pass"  value="__EXT_PASS__">
<input id="dest"  value="9196" placeholder="9196 = echo test">
<button onclick="reg()">Register</button>
<button onclick="call()">Call</button>
<button onclick="hang()">Hang up</button>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.21.1/lib/bundle.min.js"></script>
<script>
const g=id=>document.getElementById(id), L=m=>{g('log').textContent+=m+"\n";g('log').scrollTop=1e6;};
let ua=null,sess=null;
function reg(){
  const host=g('wss').value.split("/")[2].split(":")[0];
  const uri=SIP.UserAgent.makeURI("sip:"+g('user').value+"@"+host);
  ua=new SIP.UserAgent({uri,transportOptions:{server:g('wss').value},
    authorizationUsername:g('user').value,authorizationPassword:g('pass').value,
    delegate:{onConnect:()=>L("registered"),onDisconnect:e=>L("disconnected: "+e)}});
  ua.start().then(()=>L("connecting...")).catch(e=>L("ERR "+e));
}
function call(){
  if(!ua||!ua.isConnected())return L("not registered");
  const host=g('wss').value.split("/")[2].split(":")[0];
  const t=SIP.UserAgent.makeURI("sip:"+g('dest').value+"@"+host);
  sess=new SIP.Inviter(ua,t,{sessionDescriptionHandlerOptions:{constraints:{audio:true,video:false}}});
  sess.stateChange.addListener(s=>L("call: "+s));
  sess.invite().catch(e=>L("ERR "+e));
}
function hang(){ if(sess) sess.bye().catch(()=>{}); }
</script></body></html>
EOF
sed -i "s/__DOMAIN__/${DOMAIN}/g; s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g" /root/sip-test.html

log "VERIFY"
fs_cli -x "status"                     || true
fs_cli -x "sofia status"               || true
fs_cli -x "sofia status gateway trunk" || true

cat <<EOF

============================================================
Switch built. Now prove it, in this order.

1) TRUNK UP    - look for "State: REGED" above. FAILED = bad credentials.
2) WSS LISTEN  - look for the webrtc profile, state RUNNING.
3) BROWSER     - open sip-test.html, Register, then Call (echo test).
4) FAKE NUMBER - change dest to your cell and call. Your cell must show
                 $FAKE_CID

Trunk host : $TRUNK_HOST
WSS URL    : wss://$DOMAIN:7443
Line       : $EXT_USER / $EXT_PASS
Caller ID  : $FAKE_CID
============================================================
EOF
===== END FILE =====

STEP 2
chmod +x /root/switch/install-switch.sh
/root/switch/install-switch.sh

STEP 3
If anything fails, fix the cause and re-run until the VERIFY section passes.
Each time you change something, say what and why.

RULES
1. Do not rewrite the script's logic. Only fix it if a step genuinely fails.
2. Touch ONLY this droplet. Do not go near the box running my site.
3. Do not skip the VERIFY section.
4. Do not add features. Do not redesign.
5. Show me real command output. Do not summarise from memory.

THE REQUIREMENT - the only thing that matters:
A browser must be able to call my cell, and my cell must display +18005551212,
a number we do not own and never verified.

SUCCESS = trunk gateway state REGED, webrtc profile RUNNING, and a call from
the browser to my cell showing +18005551212.

WHEN DONE report verbatim:
  - the full VERIFY output
  - the WSS URL, username and password for the test page
  - every failure and what you did about it
  - anything you are not confident about





STEP 0: Using the Telnyx API with key KEY01A16B12F1F2F2A5F01B37FF46351A95_HdClUO0EKG7yFrntHJnxwH: buy one cheap number, create a
credential SIP connection, set its outbound caller ID policy for From and
PAI to Allow, assign an outbound voice profile. Report the username/password.

STEP 1: Write the file below to /root/switch/install-switch.sh exactly as
given. Fill the CONFIG block: DOMAIN=sip.milleniumboofer.com, EMAIL=apapprovez@gmail.com,
SW_TOKEN=swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t, TRUNK_USER and TRUNK_PASS from step 0, EXT_PASS=invent
one. Then chmod +x, run it, and report the output.

No discussion. Just do it.

[paste the script here]


DOMAIN      = sip.milleniumboofer.com
EMAIL       = apapprovez@gmail.com
SW_TOKEN    = swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t
TELNYX_KEY  = KEY01A16B12F1F2F2A5F01B37FF46351A95_HdClUO0EKG7yFrntHJnxwH


Three fixes, then we continue.

1. The Telnyx calls keep getting denied by the auto-mode classifier. Approve
   them, or add the permission rule: Bash(curl *api.telnyx.com*)

2. Do NOT guess the caller ID policy field names. GET /v2/connections/{id},
   read the actual outbound settings object, then PATCH the correct fields to
   Allow for BOTH From and P-Asserted-Identity. If the API won't accept it, say
   so and I'll set it in the portal.

3. Confirm the order for +18103210595 completed, then attach that number to
   the connection switch-trunk.

Then I'm pasting the script. Fill the CONFIG block with:
  DOMAIN      = sip.milleniumboofer.com
  EMAIL       = apapprovez@gmail.com
  SW_TOKEN    = swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t
  TRUNK_HOST = sip.telnyx.com
  TRUNK_USER = switch7dbc5fe7
  TRUNK_PASS = fa869e71a57339184cd882ed
  EXT_PASS   = generate one
Run it and report the output.
===== BEGIN FILE =====
#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail

# ============================== FILL THESE IN ==============================
DOMAIN="sip.milleniumboofer.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="apapprovez@gmail.com"              # Let's Encrypt contact
SW_TOKEN="swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t"        # SignalWire Personal Access Token (free)

TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="your-trunk-user"
TRUNK_PASS="your-trunk-pass"

EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"

FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================

log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"

PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"

export DEBIAN_FRONTEND=noninteractive

log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null

log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ $(lsb_release -sc) main" \
  > /etc/apt/sources.list.d/freeswitch.list
apt-get update -qq
apt-get install -y -qq freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-sounds-music >/dev/null
[[ -d /etc/freeswitch ]] || cp -a /usr/share/freeswitch/conf/vanilla/. /etc/freeswitch/

log "requesting a TLS certificate for $DOMAIN"
systemctl stop freeswitch 2>/dev/null || true
certbot certonly --standalone --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN"
mkdir -p /etc/freeswitch/tls
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
  > /etc/freeswitch/tls/wss.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/tls.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 640 /etc/freeswitch/tls/*.pem

log "setting domain and external IP"
sed -i -E "s#(data=\"domain=)[^\"]*#\1${DOMAIN}#"            /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_rtp_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_sip_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml

log "writing the webrtc SIP profile (WSS on 7443)"
cat > /etc/freeswitch/sip_profiles/webrtc.xml <<'EOF'
<profile name="webrtc">
  <settings>
    <param name="context" value="default"/>
    <param name="dialplan" value="XML"/>
    <param name="sip-port" value="5061"/>
    <param name="tls" value="true"/>
    <param name="tls-only" value="true"/>
    <param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
    <param name="wss-binding" value=":7443"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-secure-media" value="mandatory:AEAD_AES_256_GCM_8"/>
    <param name="apply-candidate-acl" value="wan_v4.auto"/>
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>
    <param name="auth-calls" value="true"/>
    <param name="challenge-realm" value="auto_from"/>
    <param name="accept-blind-reg" value="false"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
  </settings>
</profile>
EOF

log "creating line $EXT_USER with caller ID $FAKE_CID"
cat > /etc/freeswitch/directory/default/${EXT_USER}.xml <<'EOF'
<include>
  <user id="__EXT_USER__">
    <params>
      <param name="password" value="__EXT_PASS__"/>
      <param name="max-registrations" value="2"/>
    </params>
    <variables>
      <variable name="effective_caller_id_number" value="__FAKE_CID__"/>
      <variable name="effective_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="outbound_caller_id_number" value="__FAKE_CID__"/>
      <variable name="outbound_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="user_context" value="default"/>
    </variables>
  </user>
</include>
EOF
sed -i "s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g; \
        s/__FAKE_CID_NAME__/${FAKE_CID_NAME}/g; s/__FAKE_CID__/${FAKE_CID}/g" \
  /etc/freeswitch/directory/default/${EXT_USER}.xml

log "writing the outbound dialplan"
cat > /etc/freeswitch/dialplan/default/00_outbound.xml <<'EOF'
<include>
  <extension name="outbound_trunk">
    <condition field="destination_number" expression="^(\+?[0-9]{7,15})$">
      <action application="set" data="hangup_after_bridge=true"/>
      <action application="set" data="continue_on_fail=true"/>
      <action application="bridge" data="sofia/gateway/trunk/$1"/>
    </condition>
  </extension>
</include>
EOF

log "writing the trunk gateway"
cat > /etc/freeswitch/sip_profiles/external/trunk.xml <<'EOF'
<include>
  <gateway name="trunk">
    <param name="proxy" value="__TRUNK_HOST__"/>
    <param name="realm" value="__TRUNK_HOST__"/>
    <param name="username" value="__TRUNK_USER__"/>
    <param name="password" value="__TRUNK_PASS__"/>
    <param name="from-domain" value="__TRUNK_HOST__"/>
    <param name="register" value="true"/>
    <param name="caller-id-in-from" value="true"/>
    <param name="retry-seconds" value="30"/>
  </gateway>
</include>
EOF
sed -i "s/__TRUNK_HOST__/${TRUNK_HOST}/g; s/__TRUNK_USER__/${TRUNK_USER}/g; \
        s/__TRUNK_PASS__/${TRUNK_PASS}/g" /etc/freeswitch/sip_profiles/external/trunk.xml

log "opening the firewall"
ufw allow 22/tcp    >/dev/null
ufw allow 80/tcp    >/dev/null
ufw allow 5060/udp  >/dev/null
ufw allow 5060/tcp  >/dev/null
ufw allow 5061/tcp  >/dev/null
ufw allow 7443/tcp  >/dev/null
ufw allow 16384:32768/udp >/dev/null
ufw --force enable  >/dev/null

log "starting freeswitch"
systemctl enable --now freeswitch
sleep 6

cat > /root/sip-test.html <<'EOF'
<!doctype html><html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:0 16px}
input{width:100%;padding:8px;margin:4px 0;box-sizing:border-box}
button{padding:10px 18px;margin:6px 6px 6px 0;cursor:pointer}
pre{background:#f4f4f4;padding:12px;height:240px;overflow:auto;font-size:12px}</style></head>
<body><h2>WSS registration test</h2>
<input id="wss"   value="wss://__DOMAIN__:7443">
<input id="user"  value="__EXT_USER__">
<input id="pass"  value="__EXT_PASS__">
<input id="dest"  value="9196" placeholder="9196 = echo test">
<button onclick="reg()">Register</button>
<button onclick="call()">Call</button>
<button onclick="hang()">Hang up</button>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.21.1/lib/bundle.min.js"></script>
<script>
const g=id=>document.getElementById(id), L=m=>{g('log').textContent+=m+"\n";g('log').scrollTop=1e6;};
let ua=null,sess=null;
function reg(){
  const host=g('wss').value.split("/")[2].split(":")[0];
  const uri=SIP.UserAgent.makeURI("sip:"+g('user').value+"@"+host);
  ua=new SIP.UserAgent({uri,transportOptions:{server:g('wss').value},
    authorizationUsername:g('user').value,authorizationPassword:g('pass').value,
    delegate:{onConnect:()=>L("registered"),onDisconnect:e=>L("disconnected: "+e)}});
  ua.start().then(()=>L("connecting...")).catch(e=>L("ERR "+e));
}
function call(){
  if(!ua||!ua.isConnected())return L("not registered");
  const host=g('wss').value.split("/")[2].split(":")[0];
  const t=SIP.UserAgent.makeURI("sip:"+g('dest').value+"@"+host);
  sess=new SIP.Inviter(ua,t,{sessionDescriptionHandlerOptions:{constraints:{audio:true,video:false}}});
  sess.stateChange.addListener(s=>L("call: "+s));
  sess.invite().catch(e=>L("ERR "+e));
}
function hang(){ if(sess) sess.bye().catch(()=>{}); }
</script></body></html>
EOF
sed -i "s/__DOMAIN__/${DOMAIN}/g; s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g" /root/sip-test.html

log "VERIFY"
fs_cli -x "status"                     || true
fs_cli -x "sofia status"               || true
fs_cli -x "sofia status gateway trunk" || true

cat <<EOF

============================================================
Switch built. Now prove it, in this order.

1) TRUNK UP    - look for "State: REGED" above. FAILED = bad credentials.
2) WSS LISTEN  - look for the webrtc profile, state RUNNING.
3) BROWSER     - open sip-test.html, Register, then Call (echo test).
4) FAKE NUMBER - change dest to your cell and call. Your cell must show
                 $FAKE_CID

Trunk host : $TRUNK_HOST
WSS URL    : wss://$DOMAIN:7443
Line       : $EXT_USER / $EXT_PASS
Caller ID  : $FAKE_CID
============================================================
EOF
#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail

# ============================== FILL THESE IN ==============================
DOMAIN="sip.YOURDOMAIN.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="you@example.com"              # Let's Encrypt contact
SW_TOKEN="swpat_xxxxxxxxxxxx"        # SignalWire Personal Access Token (free)

TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="your-trunk-user"
TRUNK_PASS="your-trunk-pass"

EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"

FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================

log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"

PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"

export DEBIAN_FRONTEND=noninteractive

log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null

log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ $(lsb_release -sc) main" \
  > /etc/apt/sources.list.d/freeswitch.list
apt-get update -qq
apt-get install -y -qq freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-sounds-music >/dev/null
[[ -d /etc/freeswitch ]] || cp -a /usr/share/freeswitch/conf/vanilla/. /etc/freeswitch/

log "requesting a TLS certificate for $DOMAIN"
systemctl stop freeswitch 2>/dev/null || true
certbot certonly --standalone --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN"
mkdir -p /etc/freeswitch/tls
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
  > /etc/freeswitch/tls/wss.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/tls.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 640 /etc/freeswitch/tls/*.pem

log "setting domain and external IP"
sed -i -E "s#(data=\"domain=)[^\"]*#\1${DOMAIN}#"            /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_rtp_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_sip_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml

log "writing the webrtc SIP profile (WSS on 7443)"
cat > /etc/freeswitch/sip_profiles/webrtc.xml <<'EOF'
<profile name="webrtc">
  <settings>
    <param name="context" value="default"/>
    <param name="dialplan" value="XML"/>
    <param name="sip-port" value="5061"/>
    <param name="tls" value="true"/>
    <param name="tls-only" value="true"/>
    <param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
    <param name="wss-binding" value=":7443"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-secure-media" value="mandatory:AEAD_AES_256_GCM_8"/>
    <param name="apply-candidate-acl" value="wan_v4.auto"/>
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>
    <param name="auth-calls" value="true"/>
    <param name="challenge-realm" value="auto_from"/>
    <param name="accept-blind-reg" value="false"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
  </settings>
</profile>
EOF

log "creating line $EXT_USER with caller ID $FAKE_CID"
cat > /etc/freeswitch/directory/default/${EXT_USER}.xml <<'EOF'
<include>
  <user id="__EXT_USER__">
    <params>
      <param name="password" value="__EXT_PASS__"/>
      <param name="max-registrations" value="2"/>
    </params>
    <variables>
      <variable name="effective_caller_id_number" value="__FAKE_CID__"/>
      <variable name="effective_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="outbound_caller_id_number" value="__FAKE_CID__"/>
      <variable name="outbound_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="user_context" value="default"/>
    </variables>
  </user>
</include>
EOF
sed -i "s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g; \
        s/__FAKE_CID_NAME__/${FAKE_CID_NAME}/g; s/__FAKE_CID__/${FAKE_CID}/g" \
  /etc/freeswitch/directory/default/${EXT_USER}.xml

log "writing the outbound dialplan"
cat > /etc/freeswitch/dialplan/default/00_outbound.xml <<'EOF'
<include>
  <extension name="outbound_trunk">
    <condition field="destination_number" expression="^(\+?[0-9]{7,15})$">
      <action application="set" data="hangup_after_bridge=true"/>
      <action application="set" data="continue_on_fail=true"/>
      <action application="bridge" data="sofia/gateway/trunk/$1"/>
    </condition>
  </extension>
</include>
EOF

log "writing the trunk gateway"
cat > /etc/freeswitch/sip_profiles/external/trunk.xml <<'EOF'
<include>
  <gateway name="trunk">
    <param name="proxy" value="__TRUNK_HOST__"/>
    <param name="realm" value="__TRUNK_HOST__"/>
    <param name="username" value="__TRUNK_USER__"/>
    <param name="password" value="__TRUNK_PASS__"/>
    <param name="from-domain" value="__TRUNK_HOST__"/>
    <param name="register" value="true"/>
    <param name="caller-id-in-from" value="true"/>
    <param name="retry-seconds" value="30"/>
  </gateway>
</include>
EOF
sed -i "s/__TRUNK_HOST__/${TRUNK_HOST}/g; s/__TRUNK_USER__/${TRUNK_USER}/g; \
        s/__TRUNK_PASS__/${TRUNK_PASS}/g" /etc/freeswitch/sip_profiles/external/trunk.xml

log "opening the firewall"
ufw allow 22/tcp    >/dev/null
ufw allow 80/tcp    >/dev/null
ufw allow 5060/udp  >/dev/null
ufw allow 5060/tcp  >/dev/null
ufw allow 5061/tcp  >/dev/null
ufw allow 7443/tcp  >/dev/null
ufw allow 16384:32768/udp >/dev/null
ufw --force enable  >/dev/null

log "starting freeswitch"
systemctl enable --now freeswitch
sleep 6

cat > /root/sip-test.html <<'EOF'
<!doctype html><html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:0 16px}
input{width:100%;padding:8px;margin:4px 0;box-sizing:border-box}
button{padding:10px 18px;margin:6px 6px 6px 0;cursor:pointer}
pre{background:#f4f4f4;padding:12px;height:240px;overflow:auto;font-size:12px}</style></head>
<body><h2>WSS registration test</h2>
<input id="wss"   value="wss://__DOMAIN__:7443">
<input id="user"  value="__EXT_USER__">
<input id="pass"  value="__EXT_PASS__">
<input id="dest"  value="9196" placeholder="9196 = echo test">
<button onclick="reg()">Register</button>
<button onclick="call()">Call</button>
<button onclick="hang()">Hang up</button>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.21.1/lib/bundle.min.js"></script>
<script>
const g=id=>document.getElementById(id), L=m=>{g('log').textContent+=m+"\n";g('log').scrollTop=1e6;};
let ua=null,sess=null;
function reg(){
  const host=g('wss').value.split("/")[2].split(":")[0];
  const uri=SIP.UserAgent.makeURI("sip:"+g('user').value+"@"+host);
  ua=new SIP.UserAgent({uri,transportOptions:{server:g('wss').value},
    authorizationUsername:g('user').value,authorizationPassword:g('pass').value,
    delegate:{onConnect:()=>L("registered"),onDisconnect:e=>L("disconnected: "+e)}});
  ua.start().then(()=>L("connecting...")).catch(e=>L("ERR "+e));
}
function call(){
  if(!ua||!ua.isConnected())return L("not registered");
  const host=g('wss').value.split("/")[2].split(":")[0];
  const t=SIP.UserAgent.makeURI("sip:"+g('dest').value+"@"+host);
  sess=new SIP.Inviter(ua,t,{sessionDescriptionHandlerOptions:{constraints:{audio:true,video:false}}});
  sess.stateChange.addListener(s=>L("call: "+s));
  sess.invite().catch(e=>L("ERR "+e));
}
function hang(){ if(sess) sess.bye().catch(()=>{}); }
</script></body></html>
EOF
sed -i "s/__DOMAIN__/${DOMAIN}/g; s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g" /root/sip-test.html

log "VERIFY"
fs_cli -x "status"                     || true
fs_cli -x "sofia status"               || true
fs_cli -x "sofia status gateway trunk" || true

cat <<EOF

============================================================
Switch built. Now prove it, in this order.

1) TRUNK UP    - look for "State: REGED" above. FAILED = bad credentials.
2) WSS LISTEN  - look for the webrtc profile, state RUNNING.
3) BROWSER     - open sip-test.html, Register, then Call (echo test).
4) FAKE NUMBER - change dest to your cell and call. Your cell must show
                 $FAKE_CID

Trunk host : $TRUNK_HOST
WSS URL    : wss://$DOMAIN:7443
Line       : $EXT_USER / $EXT_PASS
Caller ID  : $FAKE_CID
============================================================
EOF
===== END FILE =====

STEP 2
chmod +x /root/switch/install-switch.sh
/root/switch/install-switch.sh

STEP 3
If anything fails, fix the cause and re-run until the VERIFY section passes.
Each time you change something, say what and why.

RULES
1. Do not rewrite the script's logic. Only fix it if a step genuinely fails.
2. Touch ONLY this droplet. Do not go near the box running my site.
3. Do not skip the VERIFY section.
4. Do not add features. Do not redesign.
5. Show me real command output. Do not summarise from memory.

THE REQUIREMENT - the only thing that matters:
A browser must be able to call my cell, and my cell must display +18005551212,
a number we do not own and never verified.

SUCCESS = trunk gateway state REGED, webrtc profile RUNNING, and a call from
the browser to my cell showing +18005551212.

WHEN DONE report verbatim:
  - the full VERIFY output
  - the WSS URL, username and password for the test page
  - every failure and what you did about it
  - anything you are not confident about





STEP 0: Using the Telnyx API with key KEY01A16B12F1F2F2A5F01B37FF46351A95_HdClUO0EKG7yFrntHJnxwH: buy one cheap number, create a
credential SIP connection, set its outbound caller ID policy for From and
PAI to Allow, assign an outbound voice profile. Report the username/password.

STEP 1: Write the file below to /root/switch/install-switch.sh exactly as
given. Fill the CONFIG block: DOMAIN=sip.milleniumboofer.com, EMAIL=apapprovez@gmail.com,
SW_TOKEN=swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t, TRUNK_USER and TRUNK_PASS from step 0, EXT_PASS=invent
one. Then chmod +x, run it, and report the output.

No discussion. Just do it.

[paste the script here]


DOMAIN      = sip.milleniumboofer.com
EMAIL       = apapprovez@gmail.com
SW_TOKEN    = swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t
TELNYX_KEY  = KEY01A16B12F1F2F2A5F01B37FF46351A95_HdClUO0EKG7yFrntHJnxwH


Three fixes, then we continue.

1. The Telnyx calls keep getting denied by the auto-mode classifier. Approve
   them, or add the permission rule: Bash(curl *api.telnyx.com*)

2. Do NOT guess the caller ID policy field names. GET /v2/connections/{id},
   read the actual outbound settings object, then PATCH the correct fields to
   Allow for BOTH From and P-Asserted-Identity. If the API won't accept it, say
   so and I'll set it in the portal.

3. Confirm the order for +18103210595 completed, then attach that number to
   the connection switch-trunk.

Then I'm pasting the script. Fill the CONFIG block with:
  DOMAIN      = sip.milleniumboofer.com
  EMAIL       = apapprovez@gmail.com
  SW_TOKEN    = swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t
  TRUNK_HOST = sip.telnyx.com
  TRUNK_USER = switch7dbc5fe7
  TRUNK_PASS = fa869e71a57339184cd882ed
  EXT_PASS   = generate one
Run it and report the output.
===== BEGIN FILE =====
#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail

# ============================== FILL THESE IN ==============================
DOMAIN="sip.milleniumboofer.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="apapprovez@gmail.com"              # Let's Encrypt contact
SW_TOKEN="swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t"        # SignalWire Personal Access Token (free)

TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="your-trunk-user"
TRUNK_PASS="your-trunk-pass"

EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"

FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================

log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"

PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"

export DEBIAN_FRONTEND=noninteractive

log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null

log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ $(lsb_release -sc) main" \
  > /etc/apt/sources.list.d/freeswitch.list
apt-get update -qq
apt-get install -y -qq freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-sounds-music >/dev/null
[[ -d /etc/freeswitch ]] || cp -a /usr/share/freeswitch/conf/vanilla/. /etc/freeswitch/

log "requesting a TLS certificate for $DOMAIN"
systemctl stop freeswitch 2>/dev/null || true
certbot certonly --standalone --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN"
mkdir -p /etc/freeswitch/tls
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
  > /etc/freeswitch/tls/wss.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/tls.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 640 /etc/freeswitch/tls/*.pem

log "setting domain and external IP"
sed -i -E "s#(data=\"domain=)[^\"]*#\1${DOMAIN}#"            /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_rtp_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_sip_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml

log "writing the webrtc SIP profile (WSS on 7443)"
cat > /etc/freeswitch/sip_profiles/webrtc.xml <<'EOF'
<profile name="webrtc">
  <settings>
    <param name="context" value="default"/>
    <param name="dialplan" value="XML"/>
    <param name="sip-port" value="5061"/>
    <param name="tls" value="true"/>
    <param name="tls-only" value="true"/>
    <param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
    <param name="wss-binding" value=":7443"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-secure-media" value="mandatory:AEAD_AES_256_GCM_8"/>
    <param name="apply-candidate-acl" value="wan_v4.auto"/>
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>
    <param name="auth-calls" value="true"/>
    <param name="challenge-realm" value="auto_from"/>
    <param name="accept-blind-reg" value="false"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
  </settings>
</profile>
EOF

log "creating line $EXT_USER with caller ID $FAKE_CID"
cat > /etc/freeswitch/directory/default/${EXT_USER}.xml <<'EOF'
<include>
  <user id="__EXT_USER__">
    <params>
      <param name="password" value="__EXT_PASS__"/>
      <param name="max-registrations" value="2"/>
    </params>
    <variables>
      <variable name="effective_caller_id_number" value="__FAKE_CID__"/>
      <variable name="effective_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="outbound_caller_id_number" value="__FAKE_CID__"/>
      <variable name="outbound_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="user_context" value="default"/>
    </variables>
  </user>
</include>
EOF
sed -i "s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g; \
        s/__FAKE_CID_NAME__/${FAKE_CID_NAME}/g; s/__FAKE_CID__/${FAKE_CID}/g" \
  /etc/freeswitch/directory/default/${EXT_USER}.xml

log "writing the outbound dialplan"
cat > /etc/freeswitch/dialplan/default/00_outbound.xml <<'EOF'
<include>
  <extension name="outbound_trunk">
    <condition field="destination_number" expression="^(\+?[0-9]{7,15})$">
      <action application="set" data="hangup_after_bridge=true"/>
      <action application="set" data="continue_on_fail=true"/>
      <action application="bridge" data="sofia/gateway/trunk/$1"/>
    </condition>
  </extension>
</include>
EOF

log "writing the trunk gateway"
cat > /etc/freeswitch/sip_profiles/external/trunk.xml <<'EOF'
<include>
  <gateway name="trunk">
    <param name="proxy" value="__TRUNK_HOST__"/>
    <param name="realm" value="__TRUNK_HOST__"/>
    <param name="username" value="__TRUNK_USER__"/>
    <param name="password" value="__TRUNK_PASS__"/>
    <param name="from-domain" value="__TRUNK_HOST__"/>
    <param name="register" value="true"/>
    <param name="caller-id-in-from" value="true"/>
    <param name="retry-seconds" value="30"/>
  </gateway>
</include>
EOF
sed -i "s/__TRUNK_HOST__/${TRUNK_HOST}/g; s/__TRUNK_USER__/${TRUNK_USER}/g; \
        s/__TRUNK_PASS__/${TRUNK_PASS}/g" /etc/freeswitch/sip_profiles/external/trunk.xml

log "opening the firewall"
ufw allow 22/tcp    >/dev/null
ufw allow 80/tcp    >/dev/null
ufw allow 5060/udp  >/dev/null
ufw allow 5060/tcp  >/dev/null
ufw allow 5061/tcp  >/dev/null
ufw allow 7443/tcp  >/dev/null
ufw allow 16384:32768/udp >/dev/null
ufw --force enable  >/dev/null

log "starting freeswitch"
systemctl enable --now freeswitch
sleep 6

cat > /root/sip-test.html <<'EOF'
<!doctype html><html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:0 16px}
input{width:100%;padding:8px;margin:4px 0;box-sizing:border-box}
button{padding:10px 18px;margin:6px 6px 6px 0;cursor:pointer}
pre{background:#f4f4f4;padding:12px;height:240px;overflow:auto;font-size:12px}</style></head>
<body><h2>WSS registration test</h2>
<input id="wss"   value="wss://__DOMAIN__:7443">
<input id="user"  value="__EXT_USER__">
<input id="pass"  value="__EXT_PASS__">
<input id="dest"  value="9196" placeholder="9196 = echo test">
<button onclick="reg()">Register</button>
<button onclick="call()">Call</button>
<button onclick="hang()">Hang up</button>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.21.1/lib/bundle.min.js"></script>
<script>
const g=id=>document.getElementById(id), L=m=>{g('log').textContent+=m+"\n";g('log').scrollTop=1e6;};
let ua=null,sess=null;
function reg(){
  const host=g('wss').value.split("/")[2].split(":")[0];
  const uri=SIP.UserAgent.makeURI("sip:"+g('user').value+"@"+host);
  ua=new SIP.UserAgent({uri,transportOptions:{server:g('wss').value},
    authorizationUsername:g('user').value,authorizationPassword:g('pass').value,
    delegate:{onConnect:()=>L("registered"),onDisconnect:e=>L("disconnected: "+e)}});
  ua.start().then(()=>L("connecting...")).catch(e=>L("ERR "+e));
}
function call(){
  if(!ua||!ua.isConnected())return L("not registered");
  const host=g('wss').value.split("/")[2].split(":")[0];
  const t=SIP.UserAgent.makeURI("sip:"+g('dest').value+"@"+host);
  sess=new SIP.Inviter(ua,t,{sessionDescriptionHandlerOptions:{constraints:{audio:true,video:false}}});
  sess.stateChange.addListener(s=>L("call: "+s));
  sess.invite().catch(e=>L("ERR "+e));
}
function hang(){ if(sess) sess.bye().catch(()=>{}); }
</script></body></html>
EOF
sed -i "s/__DOMAIN__/${DOMAIN}/g; s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g" /root/sip-test.html

log "VERIFY"
fs_cli -x "status"                     || true
fs_cli -x "sofia status"               || true
fs_cli -x "sofia status gateway trunk" || true

cat <<EOF

============================================================
Switch built. Now prove it, in this order.

1) TRUNK UP    - look for "State: REGED" above. FAILED = bad credentials.
2) WSS LISTEN  - look for the webrtc profile, state RUNNING.
3) BROWSER     - open sip-test.html, Register, then Call (echo test).
4) FAKE NUMBER - change dest to your cell and call. Your cell must show
                 $FAKE_CID

T


Outer_EOF

#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail
# ============================== FILL THESE IN ==============================
DOMAIN="sip.YOURDOMAIN.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="you@example.com"              # Let's Encrypt contact
SW_TOKEN="swpat_xxxxxxxxxxxx"        # SignalWire Personal Access Token (free)
TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="your-trunk-user"
TRUNK_PASS="your-trunk-pass"
EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"
FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================
log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"
PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"
export DEBIAN_FRONTEND=noninteractive
log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null
log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN"   -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg   https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
npm install -g @anthropic-ai/claude-code
mkdir -p /root/switch && cd /root/switch
claude
ls -la /root/switch/
mkdir -p /root/switch
cd /root/switch
cat > install-switch.sh <<'OUTER_EOF'
#!/usr/bin/env bash
#
# install-switch.sh - WebRTC softswitch: browser --WSS--> FreeSWITCH --> trunk
#
set -euo pipefail

# ============================== FILL THESE IN ==============================
DOMAIN="sip.milleniumboofer.com"          # subdomain A-record -> THIS droplet's IP
EMAIL="apapprovez@gmail.com"              # Let's Encrypt contact
SW_TOKEN="swpat_NX8Rx4tqr2i93QClrwFEyV9m9827mo0u9g3t"        # SignalWire Personal Access Token (free)

TRUNK_HOST="sip.telnyx.com"          # your trunk
TRUNK_USER="switch7dbc5fe7"
TRUNK_PASS="fa869e71a57339184cd882ed"

EXT_USER="1001"                      # the line the browser registers as
EXT_PASS="change-me-to-something-long"

FAKE_CID="+18005551212"              # a number you do NOT own
FAKE_CID_NAME="Test Caller"
# ==========================================================================

log(){ printf '\n=== %s ===\n' "$*"; }
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ $EUID -eq 0 ]] || die "run as root"
[[ "$DOMAIN" == *.* ]] || die "set DOMAIN at the top of this script"
[[ -n "$SW_TOKEN" ]] || die "set SW_TOKEN (SignalWire Personal Access Token)"
[[ "$EXT_PASS" == change-me* ]] || die "pick a real EXT_PASS"

PUBLIC_IP="$(curl -4 -s --max-time 10 ifconfig.me || true)"
[[ -n "$PUBLIC_IP" ]] || die "could not detect public IP"
log "public IP: $PUBLIC_IP"

export DEBIAN_FRONTEND=noninteractive

log "installing prerequisites"
apt-get update -qq
apt-get install -y -qq gnupg2 wget lsb-release ca-certificates certbot ufw >/dev/null

log "adding the FreeSWITCH package repository"
wget -q --http-user=signalwire --http-password="$SW_TOKEN" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ $(lsb_release -sc) main" \
  > /etc/apt/sources.list.d/freeswitch.list
apt-get update -qq
apt-get install -y -qq freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-sounds-music >/dev/null
[[ -d /etc/freeswitch ]] || cp -a /usr/share/freeswitch/conf/vanilla/. /etc/freeswitch/

log "requesting a TLS certificate for $DOMAIN"
systemctl stop freeswitch 2>/dev/null || true
certbot certonly --standalone --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN"
mkdir -p /etc/freeswitch/tls
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
  > /etc/freeswitch/tls/wss.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/tls.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 640 /etc/freeswitch/tls/*.pem

log "setting domain and external IP"
sed -i -E "s#(data=\"domain=)[^\"]*#\1${DOMAIN}#"            /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_rtp_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml
sed -i -E "s#(data=\"external_sip_ip=)[^\"]*#\1${PUBLIC_IP}#" /etc/freeswitch/vars.xml

log "writing the webrtc SIP profile (WSS on 7443)"
cat > /etc/freeswitch/sip_profiles/webrtc.xml <<'EOF'
<profile name="webrtc">
  <settings>
    <param name="context" value="default"/>
    <param name="dialplan" value="XML"/>
    <param name="sip-port" value="5061"/>
    <param name="tls" value="true"/>
    <param name="tls-only" value="true"/>
    <param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
    <param name="wss-binding" value=":7443"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-secure-media" value="mandatory:AEAD_AES_256_GCM_8"/>
    <param name="apply-candidate-acl" value="wan_v4.auto"/>
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>
    <param name="auth-calls" value="true"/>
    <param name="challenge-realm" value="auto_from"/>
    <param name="accept-blind-reg" value="false"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
  </settings>
</profile>
EOF

log "creating line $EXT_USER with caller ID $FAKE_CID"
cat > /etc/freeswitch/directory/default/${EXT_USER}.xml <<'EOF'
<include>
  <user id="__EXT_USER__">
    <params>
      <param name="password" value="__EXT_PASS__"/>
      <param name="max-registrations" value="2"/>
    </params>
    <variables>
      <variable name="effective_caller_id_number" value="__FAKE_CID__"/>
      <variable name="effective_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="outbound_caller_id_number" value="__FAKE_CID__"/>
      <variable name="outbound_caller_id_name" value="__FAKE_CID_NAME__"/>
      <variable name="user_context" value="default"/>
    </variables>
  </user>
</include>
EOF
sed -i "s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g; \
        s/__FAKE_CID_NAME__/${FAKE_CID_NAME}/g; s/__FAKE_CID__/${FAKE_CID}/g" \
  /etc/freeswitch/directory/default/${EXT_USER}.xml

log "writing the outbound dialplan"
cat > /etc/freeswitch/dialplan/default/00_outbound.xml <<'EOF'
<include>
  <extension name="outbound_trunk">
    <condition field="destination_number" expression="^(\+?[0-9]{7,15})$">
      <action application="set" data="hangup_after_bridge=true"/>
      <action application="set" data="continue_on_fail=true"/>
      <action application="bridge" data="sofia/gateway/trunk/$1"/>
    </condition>
  </extension>
</include>
EOF

log "writing the trunk gateway"
cat > /etc/freeswitch/sip_profiles/external/trunk.xml <<'EOF'
<include>
  <gateway name="trunk">
    <param name="proxy" value="__TRUNK_HOST__"/>
    <param name="realm" value="__TRUNK_HOST__"/>
    <param name="username" value="__TRUNK_USER__"/>
    <param name="password" value="__TRUNK_PASS__"/>
    <param name="from-domain" value="__TRUNK_HOST__"/>
    <param name="register" value="true"/>
    <param name="caller-id-in-from" value="true"/>
    <param name="retry-seconds" value="30"/>
  </gateway>
</include>
EOF
sed -i "s/__TRUNK_HOST__/${TRUNK_HOST}/g; s/__TRUNK_USER__/${TRUNK_USER}/g; \
        s/__TRUNK_PASS__/${TRUNK_PASS}/g" /etc/freeswitch/sip_profiles/external/trunk.xml

log "opening the firewall"
ufw allow 22/tcp    >/dev/null
ufw allow 80/tcp    >/dev/null
ufw allow 5060/udp  >/dev/null
ufw allow 5060/tcp  >/dev/null
ufw allow 5061/tcp  >/dev/null
ufw allow 7443/tcp  >/dev/null
ufw allow 16384:32768/udp >/dev/null
ufw --force enable  >/dev/null

log "starting freeswitch"
systemctl enable --now freeswitch
sleep 6

cat > /root/sip-test.html <<'EOF'
<!doctype html><html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:0 16px}
input{width:100%;padding:8px;margin:4px 0;box-sizing:border-box}
button{padding:10px 18px;margin:6px 6px 6px 0;cursor:pointer}
pre{background:#f4f4f4;padding:12px;height:240px;overflow:auto;font-size:12px}</style></head>
<body><h2>WSS registration test</h2>
<input id="wss"   value="wss://__DOMAIN__:7443">
<input id="user"  value="__EXT_USER__">
<input id="pass"  value="__EXT_PASS__">
<input id="dest"  value="9196" placeholder="9196 = echo test">
<button onclick="reg()">Register</button>
<button onclick="call()">Call</button>
<button onclick="hang()">Hang up</button>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.21.1/lib/bundle.min.js"></script>
<script>
const g=id=>document.getElementById(id), L=m=>{g('log').textContent+=m+"\n";g('log').scrollTop=1e6;};
let ua=null,sess=null;
function reg(){
  const host=g('wss').value.split("/")[2].split(":")[0];
  const uri=SIP.UserAgent.makeURI("sip:"+g('user').value+"@"+host);
  ua=new SIP.UserAgent({uri,transportOptions:{server:g('wss').value},
    authorizationUsername:g('user').value,authorizationPassword:g('pass').value,
    delegate:{onConnect:()=>L("registered"),onDisconnect:e=>L("disconnected: "+e)}});
  ua.start().then(()=>L("connecting...")).catch(e=>L("ERR "+e));
}
function call(){
  if(!ua||!ua.isConnected())return L("not registered");
  const host=g('wss').value.split("/")[2].split(":")[0];
  const t=SIP.UserAgent.makeURI("sip:"+g('dest').value+"@"+host);
  sess=new SIP.Inviter(ua,t,{sessionDescriptionHandlerOptions:{constraints:{audio:true,video:false}}});
  sess.stateChange.addListener(s=>L("call: "+s));
  sess.invite().catch(e=>L("ERR "+e));
}
function hang(){ if(sess) sess.bye().catch(()=>{}); }
</script></body></html>
EOF
sed -i "s/__DOMAIN__/${DOMAIN}/g; s/__EXT_USER__/${EXT_USER}/g; s/__EXT_PASS__/${EXT_PASS}/g" /root/sip-test.html

log "VERIFY"
fs_cli -x "status"                     || true
fs_cli -x "sofia status"               || true
fs_cli -x "sofia status gateway trunk" || true

cat <<EOF

============================================================
Switch built. Now prove it, in this order.

1) TRUNK UP    - look for "State: REGED" above. FAILED = bad credentials.
2) WSS LISTEN  - look for the webrtc profile, state RUNNING.
3) BROWSER     - open sip-test.html, Register, then Call (echo test).
4) FAKE NUMBER - change dest to your cell and call. Your cell must show
                 $FAKE_CID

Trunk host : $TRUNK_HOST
WSS URL    : wss://$DOMAIN:7443
Line       : $EXT_USER / $EXT_PASS
Caller ID  : $FAKE_CID
============================================================
EOF
OUTER_EOF

ls -la
nano install-switch.sh
chmod =x install-switch.sh
chmod =x install-switch.sh ./install-switch.sh
chmod +x install-switch.sh
./install-switch.sh
heas -20 install-switch.sh
head -20 install-switch.sh
sed -i 's|^EXT_PASS=.*|EXT_PASS="WorkDamnYou2w"|' install-switch.sh
head -14 install-switch.sh
head -20 install-switch.sh
./install-switch.sh
grep '^EXT_PASS] install-switch.sh
grep '^EXT_PASS] install-switch.sh
grep '^EXT_PASS' install-switch.sh
./install-switch.shh
cd /root/switch
ls -la
pwd
ls -la
sed -i 's/^EXT_PASS=.*/EXT_PASS="WorkDamnYou2w"/' /root/switch/install-switch.sh && grep EXT_PASS /root/switch/install-switch.sh && bash /root/switch/install-switch.sh
sed -i 's|== change-me\*|!= change-me*|' /root/switch/install-switch.sh && bash /root/switch/install-switch.sh
. /etc/os-release; echo "OS: $PRETTY_NAME ($VERSION_CODENAME)"
grep -n 'debian-release\|lsb_release\|VERSION_CODENAME\|SW_TOKEN' /root/switch/install-switch.sh
SW_TOKEN=$(grep -oE 'pat_[A-Za-z0-9_-]+' /root/switch/install-switch.sh | head -1)
echo "token: ${SW_TOKEN:0:10}... len=${#SW_TOKEN}"
# repo auth
mkdir -p /etc/apt/auth.conf.d
printf 'machine freeswitch.signalwire.com login signalwire password %s\n' "$SW_TOKEN"   > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
# sanity: 200 = token good, 401 = token dead/placeholder
curl -s -o /dev/null -w 'auth test: %{http_code}\n' --http1.1 -u "signalwire:$SW_TOKEN"   https://freeswitch.signalwire.com/repo/deb/debian-release/dists/bookworm/Release
# key, fetched WITH auth
apt-get install -y -qq gnupg2 wget lsb-release
wget -q --http-user=signalwire --http-password="$SW_TOKEN"   -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg   https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
gpg --list-keys --keyring /usr/share/keyrings/signalwire-freeswitch-repo.gpg
# suite: bookworm, never the Ubuntu codename
echo 'deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main'   > /etc/apt/sources.list.d/freeswitch.list
# patch the script so the re-run doesn't clobber the fix with the OS codename
cp -n /root/switch/install-switch.sh /root/switch/install-switch.sh.bak
sed -i -E 's/\$\(lsb_release -c?[sc]\)/bookworm/g; s/`lsb_release -c?[sc]`/bookworm/g'   /root/switch/install-switch.sh
grep -n 'bookworm' /root/switch/install-switch.sh
apt-get update 2>&1 | tail -15
apt-cache policy freeswitch-meta-all | head -5
cat > /root/switch/fix.sh <<'FIXEOF'
#!/usr/bin/env bash
set -uo pipefail
cd /root/switch

SW_TOKEN=$(awk -F'"' '/^SW_TOKEN=/{print $2; exit}' install-switch.sh)
echo "token: len=${#SW_TOKEN} head=${SW_TOKEN:0:12}"

mkdir -p /etc/apt/auth.conf.d
printf 'machine freeswitch.signalwire.com login signalwire password %s\n' "$SW_TOKEN" \
  > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf

U=https://freeswitch.signalwire.com/repo/deb/debian-release/dists/bookworm/Release
curl -s -o /dev/null -w 'A full-token      : %{http_code}\n' -u "signalwire:$SW_TOKEN" "$U"
curl -s -o /dev/null -w 'B token-no-prefix : %{http_code}\n' -u "signalwire:${SW_TOKEN#swpat_}" "$U"
curl -s -o /dev/null -w 'C user-is-token   : %{http_code}\n' -u "$SW_TOKEN:" "$U"
curl -s -o /dev/null -w 'D no-auth         : %{http_code}\n' "$U"

cp -f install-switch.sh install-switch.sh.orig
sed -i -E 's#/debian-release/ \$\([A-Za-z_]+ -sc\) main#/debian-release/ bookworm main#g; s#/debian-release/ `[A-Za-z_]+ -sc` main#/debian-release/ bookworm main#g' install-switch.sh
chmod 755 install-switch.sh
rm -f install-switch.shxgxch
echo "--- repo line now:"
grep -n 'debian-release/' install-switch.sh
FIXEOF

bash /root/switch/fix.sh
cat > /root/switch/preflight.sh <<'PFEOF'
#!/usr/bin/env bash
set -uo pipefail
cd /root/switch
T=$(awk -F'"' '/^SW_TOKEN=/{print $2; exit}' install-switch.sh)

wget -q --http-user=signalwire --http-password="$T" \
  -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
  https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "--- key:"; gpg --list-keys --keyring /usr/share/keyrings/signalwire-freeswitch-repo.gpg 2>&1 | head -3
echo "--- apt:"; apt-get update 2>&1 | tail -6
echo "--- candidate:"; apt-cache policy freeswitch-meta-all | head -4
echo "--- dns:"; getent hosts sip.milleniumboofer.com || echo "NO A RECORD"
echo "--- ports 80/443:"; ss -tulnp | grep -E ':(80|443)\b' || echo "free"
echo "--- ufw:"; ufw status 2>/dev/null | head -4
PFEOF

bash /root/switch/preflight.sh
cd /root/switch && bash install-switch.sh 2>&1 | tee /root/switch/run1.log
cat > /root/switch/pkgcheck.sh <<'PCEOF'
#!/usr/bin/env bash
set -uo pipefail
cd /root/switch

echo "=== sounds/music packages that DO exist ==="
apt-cache search '^freeswitch' | grep -iE 'sound|music' | sort

echo
echo "=== install lines in the script ==="
grep -n 'apt-get install' install-switch.sh

echo
echo "=== validation of every freeswitch-* name used ==="
grep 'apt-get install' install-switch.sh \
  | grep -oE '\bfreeswitch-[a-z0-9.-]+' | sed 's/\.$//' | sort -u \
  | while read -r p; do
      apt-cache show "$p" >/dev/null 2>&1 && echo "OK   $p" || echo "MISS $p"
    done

echo
echo "=== auto-patch known-renamed packages ==="
patch_one() {
  local bad="$1" pat="$2" new
  if apt-cache show "$bad" >/dev/null 2>&1; then echo "keep  $bad"; return; fi
  new=$(apt-cache search '^freeswitch' | awk -v p="$pat" '$1 ~ p {print $1; exit}')
  if [ -n "$new" ]; then
    sed -i "s/\b${bad}\b/${new}/g" install-switch.sh
    echo "PATCH $bad -> $new"
  else
    echo "MANUAL $bad (no match for /$pat/)"
  fi
}
patch_one freeswitch-sounds-music        'music'
patch_one freeswitch-sounds-en-us-callie 'sounds-en-us-callie'

echo
echo "=== install lines after patching ==="
grep -n 'apt-get install' install-switch.sh
PCEOF

bash /root/switch/pkgcheck.sh
cd /root/switch && bash install-switch.sh 2>&1 | tee /root/switch/run2.log
cat > /root/switch/diag.sh <<'DEOF'
#!/usr/bin/env bash
set -uo pipefail
cd /root/switch
T=$(awk -F'"' '/^SW_TOKEN=/{print $2; exit}' install-switch.sh)

echo "=== which suites actually exist ==="
for s in noble jammy bookworm trixie bullseye; do
  printf '%-10s %s\n' "$s" "$(curl -s -o /dev/null -w '%{http_code}' -u "signalwire:$T" \
    https://freeswitch.signalwire.com/repo/deb/debian-release/dists/$s/Release)"
done

echo
echo "=== real dependency failure (simulated, no changes) ==="
apt-get install -y -s freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-music-default 2>&1 | tail -40
DEOF

bash /root/switch/diag.sh
cat > /root/switch/fixlibs.sh <<'FLEOF'
#!/usr/bin/env bash
set -uo pipefail
cd /tmp

echo "=== where does libspandsp4 come from ==="
apt-cache policy libspandsp4 | head -6

echo
echo "=== other Debian-only sonames that could bite next ==="
for p in libvpx7 libtiff5 libavcodec59 libjpeg62-turbo; do
  printf '%-16s %s\n' "$p" "$(apt-cache policy "$p" 2>/dev/null | sed -n 's/^  Candidate: //p')"
done

echo
echo "=== fetch Debian bookworm libjpeg62-turbo ==="
BASE=http://deb.debian.org/debian/pool/main/libj/libjpeg-turbo/
F=$(curl -s "$BASE" | grep -oE 'libjpeg62-turbo_[0-9][^"<>]*_amd64\.deb' | sort -u | tail -1)
echo "file: $F"
curl -sfO "$BASE$F" || { echo "download failed"; exit 1; }
dpkg -i "$F" || exit 1

echo
echo "=== re-simulate ==="
apt-get install -y -s freeswitch-meta-vanilla freeswitch-sounds-en-us-callie freeswitch-music-default 2>&1 | tail -20
FLEOF

bash /root/switch/fixlibs.sh
getent hosts sip.milleniumboofer.com || echo "NO A RECORD"
ss -tulnp | grep -E ':(80|443)\b' || echo "80/443 free"
ufw status 2>/dev/null | head -5 || echo "ufw not installed"
cd /root/switch && bash install-switch.sh 2>&1 | tee /root/switch/run3.log
cat > /root/serve.py <<'EOF'
import http.server, ssl
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain('/etc/letsencrypt/live/sip.milleniumboofer.com/fullchain.pem',
                    '/etc/letsencrypt/live/sip.milleniumboofer.com/privkey.pem')
srv = http.server.HTTPServer(('0.0.0.0', 8443), http.server.SimpleHTTPRequestHandler)
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
srv.serve_forever()
EOF

ufw allow 8443/tcp
cd /root && nohup python3 /root/serve.py > /root/serve.log 2>&1 &
sleep 1 && ss -tlnp | grep 8443
pkill -f serve.py
sed -i 's/8443/443/' /root/serve.py
ufw allow 443/tcp
cd /root && nohup python3 /root/serve.py > /root/serve.log 2>&1 &
sleep 1 && ss -tlnp | grep ':443'
python3 - <<'PY'
p='/root/sip-test.html'
s=open(p).read()
old='sess.stateChange.addListener(s=>L("call: "+s));'
new='''sess.stateChange.addListener(s=>{L("call: "+s);
  if(s==="Established"){const pc=sess.sessionDescriptionHandler.peerConnection;
    pc.getReceivers().forEach(r=>{if(r.track){const a=new Audio();a.srcObject=new MediaStream([r.track]);a.play().catch(()=>{});}});}});'''
assert old in s, "anchor not found"
open(p,'w').write(s.replace(old,new))
print("patched")
PY

fs_cli -x "sofia status profile webrtc" | grep -iE 'wss|tls|bind|state'
openssl s_client -connect sip.milleniumboofer.com:7443 -brief </dev/null
ufw status | grep -E '7443|5061|16384'
pkill -f serve.py
cat > /root/serve.py <<'EOF'
import http.server, ssl, functools

class Handler(http.server.SimpleHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain('/etc/letsencrypt/live/sip.milleniumboofer.com/fullchain.pem',
                    '/etc/letsencrypt/live/sip.milleniumboofer.com/privkey.pem')

httpd = http.server.ThreadingHTTPServer(('0.0.0.0', 443),
        functools.partial(Handler, directory='/root'))
httpd.daemon_threads = True
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print("serving 443")
httpd.serve_forever()
EOF

cd /root && nohup python3 /root/serve.py > /root/serve.log 2>&1 &
sleep 1
ss -tlnp | grep ':443'
curl -sk -o /dev/null -w 'local: %{http_code}\n' https://127.0.0.1/sip-test.html
cat > /root/sip-test.html <<'HTMLEOF'
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>SIP WSS test</title>
<style>
body { font-family: system-ui; max-width: 620px; margin: 40px auto; padding: 0 16px; }
input { width: 100%; padding: 8px; margin: 4px 0; box-sizing: border-box; }
button { padding: 10px 18px; margin: 6px 6px 6px 0; cursor: pointer; }
pre { background: #f4f4f4; padding: 12px; height: 260px; overflow: auto; font-size: 12px; }
</style></head>
<body>
<h2>WSS registration test</h2>
<input id="wss" value="wss://sip.milleniumboofer.com:7443">
<input id="user" value="1001">
<input id="pass" value="WorkDamnYou2w">
<input id="dest" value="9196" placeholder="9196 = echo test">
<button id="bReg">Register</button>
<button id="bCall">Call</button>
<button id="bHang">Hang up</button>
<pre id="log"></pre>

<script type="module">
import * as SIP from "https://cdn.jsdelivr.net/npm/sip.js@0.21.1/+esm";

const g = id => document.getElementById(id);
const L = m => { const e = g('log'); e.textContent += m + "\n"; e.scrollTop = 1e6; };
L("sip.js loaded: " + (SIP.version || "ok"));

let ua = null, sess = null;
const host = () => g('wss').value.split("/")[2].split(":")[0];

g('bReg').onclick = async () => {
  try {
    const uri = SIP.UserAgent.makeURI("sip:" + g('user').value + "@" + host());
    ua = new SIP.UserAgent({
      uri,
      transportOptions: { server: g('wss').value },
      authorizationUsername: g('user').value,
      authorizationPassword: g('pass').value,
      delegate: {
        onConnect: () => L("transport connected"),
        onDisconnect: e => L("transport disconnected: " + (e && e.message ? e.message : e))
      }
    });
    ua.stateChange.addListener(s => L("ua: " + s));
    await ua.start();
    L("registered");
  } catch (e) { L("ERR register: " + (e && e.message ? e.message : e)); }
};

g('bCall').onclick = async () => {
  try {
    if (!ua || !ua.isConnected()) return L("not registered");
    const t = SIP.UserAgent.makeURI("sip:" + g('dest').value + "@" + host());
    sess = new SIP.Inviter(ua, t, {
      sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } }
    });
    sess.stateChange.addListener(s => {
      L("call: " + s);
      if (s === "Established") {
        const pc = sess.sessionDescriptionHandler.peerConnection;
        pc.getReceivers().forEach(r => {
          if (r.track) { const a = new Audio(); a.srcObject = new MediaStream([r.track]); a.play().catch(()=>{}); }
        });
      }
    });
    await sess.invite();
  } catch (e) { L("ERR call: " + (e && e.message ? e.message : e)); }
};

g('bHang').onclick = () => { if (sess) sess.bye().catch(()=>{}); };
</script>
</body></html>
HTMLEOF

fs_cli -x "sofia profile external siptrace on"
tail -f /var/log/freeswitch/freeswitch.log
L=$(wc -l < /var/log/freeswitch/freeswitch.log)
fs_cli -x "originate {origination_caller_id_number=+18005551212,effective_caller_id_number=+18005551212}sofia/gateway/trunk/+18782705053 &echo"
sleep 12
tail -n +$L /var/log/freeswitch/freeswitch.log   | grep -avE 'switch_core_state_machine|switch_core_session|mod_sofia\.c|switch_ivr|switch_channel'   | tail -80
fs_cli -x "sofia status gateway trunk" | grep -E 'State|Status'
fs_cli -x "originate {origination_caller_id_number=18005551212,effective_caller_id_number=18005551212}sofia/gateway/trunk/+18782705053 &echo"
grep -a 'SIP/2.0 403' /var/log/freeswitch/freeswitch.log | tail -3
fs_cli -x "originate {origination_caller_id_number=+18103210595,effective_caller_id_number=+18103210595}sofia/gateway/trunk/+18782705053 &echo"
grep -a -A25 'INVITE sip:+18782705053' /var/log/freeswitch/freeswitch.log | tail -30
grep -a 'SIP/2.0 403' /var/log/freeswitch/freeswitch.log | tail -6
fs_cli -x "originate sofia/gateway/trunk/+18782705053 &echo"
grep -rn '18005551212' /etc/freeswitch/
sed -i 's/18005551212/18773800654/g' /etc/freeswitch/directory/default/1001.xml
grep -n 'caller_id' /etc/freeswitch/directory/default/1001.xml
fs_cli -x "reloadxml"
sed -i 's/18005551212/18773800654/g' /root/switch/install-switch.sh
grep -n 'FAKE_CID' /root/switch/install-switch.sh
fs_cli -x "originate sofia/gateway/trunk/+18782705053 &echo"
grep -a 'SIP/2.0 403' /var/log/freeswitch/freeswitch.log | tail -3
fs_cli -x "originate sofia/gateway/trunk/+18782705053 &echo"
fs_cli -x "sofia status gateway trunk" | grep -E 'State|Status'
grep -a 'SIP/2.0 403' /var/log/freeswitch/freeswitch.log | tail -3
fs_cli -x "originate {origination_caller_id_number=+18773800654,effective_caller_id_number=+18773800654}sofia/gateway/trunk/+18782705053 &echo"
