Commit Graph
15 Commits
Author SHA1 Message Date
Claude 90da2f5a91 Rename Asterisk container from easy-asterisk to asterisk
New installs now name the container "asterisk", matching every other
service's container_name == service name convention, instead of
reusing the vendored easy-asterisk CLI tool's own name (which stays
/usr/local/bin/easy-asterisk inside the container, unrelated and
unchanged).

An existing "easy-asterisk" install is never silently renamed: every
place that resolves the container name (_asterisk_resolve_layout in
asterisk.sh, plus the duplicated copies in security-dashboard.sh,
sms-inbound.sh, pstn-trunk.sh, and tools/pstn-test-check.sh's docker ps
detection) now reads it from the box's own docker-compose.yml instead
of assuming it, falling back to "asterisk" only when there's no
existing install to read. Migrating a live box to the new name is a
one-time manual action (edit docker-compose.yml's container_name for
Asterisk and its coturn sidecar, docker compose down + up -d); every
sibling service then picks it up automatically on its next run.

The DigitalOcean-droplet layout (asterisk-digital-ocean directory,
easy-asterisk-do container) is untouched by this - that naming stays
exactly as documented for pre-merge droplet installs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-22 03:20:36 +00:00
Claude b0d51f0344 Fix sms-inbound.sh persisting an unusable "<your-domain>" webhook URL
When the "Public domain for the webhook" prompt was left blank (DNS not
ready yet, or just missed), the installer built the Forward-to-URL as
literal https://<your-domain>/sms/... and persisted that placeholder to
settings.env as if it were real. "Update" mode never re-prompts for the
domain (by design — it's meant to leave already-configured settings
alone), so every later re-run silently re-served the same unusable
placeholder, with nothing indicating anything was wrong. A DID provider
(Anveo) correctly rejects it — it isn't a resolvable hostname.

- Only build FORWARD_URL when a real domain was entered; leave it empty
  otherwise instead of substituting the placeholder.
- Fresh-install summary and README now say plainly that setup isn't
  complete and how to finish it, instead of printing an empty/bogus URL.
- Update-mode now detects a missing/placeholder domain and tells you to
  re-run with "Full reinstall" to be asked again, instead of reporting
  success with a broken URL.

Verified with a direct test of _sms_write_readme() and the FORWARD_URL
construction for both the blank- and real-domain cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:13:16 +00:00
Claude 1fc0a6edfe Apply the local/remote Caddy mode resolution to every service, not just traccar
traccar.sh's caddy_net wiring was fixed to mirror configure_caddy_for_service's
own mode resolution (CADDY_MODE from site config, then a local ~/docker/caddy,
then the legacy CADDY_REMOTE_HOST var) instead of only checking for the local
directory. That same bare directory check was copy-pasted into the caddy_net
wiring of every other Docker service in the repo, so a site with Caddy on a
different box would silently fail to join any of their containers to caddy_net
during setup (or, for homeassistant/koha, only get half the wiring right).

Applied the same fix mechanically across all 37 services using the standard
_CADDY_NET_BLOCK/_CADDY_NET_SECTION pattern (verified identical text via
scripted diff before touching any of them), plus by hand for:
- homeassistant.sh and koha.sh, which use their own differently-shaped
  variables (HA_CADDY_NET_LINES / _CADDY_NET_ENTRY) for the same decision
- paintplus.sh and ai-stack.sh, which do a live `docker network connect`
  instead of a compose network block
- watchyourlan.sh, whose Caddy note was worded for local-only setups

sms-inbound.sh got more than a mode swap: its Caddy wiring was hand-rolled
(not routed through configure_caddy_for_service) and had no remote-Caddy
path at all — a remote Caddy box would get a misleading "Caddy isn't
installed here" message instead of a snippet. Added
_sms_write_caddy_snippet(), mirroring the snippet-file pattern
configure_caddy_for_service uses everywhere else, and pointed the firewall
gate at the same three-way mode instead of a two-way dir check.

Verified: bash -n across all of services/*.sh, a scripted check that every
touched file has exactly one _CADDY_MODE resolution and no leftover bare
`[ -d "$DOCKER_DIR/caddy" ]` feeding a caddy_net decision, and spot-checked
docker compose config renders (traccar, mattermost) confirming the ${VAR}
interpolation and multi-service usage sites still resolve correctly.
2026-08-03 18:54:02 +00:00
Claude a74da52760 Confirm inbound SMS→AMI working live; rule out outbound over SIP
Inbound: a real text through the full path (Anveo webhook -> relay ->
AMI MessageSend -> Sipnetic) landed with a SIP 200 OK, confirmed live.
Removes the last "UNVERIFIED"/"believed correct" hedges from
sms-inbound.sh now that the AMI permission class and the
Destination-not-To fix are both proven, not just plausible.

Outbound over SIP: tested directly by sending a MESSAGE toward Anveo's
trunk (the mirror image of the inbound webhook). Anveo's SBC responded
501 Not Implemented -- a real, unambiguous rejection of the method
itself. Closes off this avenue for good, symmetric with inbound
SMS-over-SIP already being confirmed unavailable on this DID: neither
direction is offered on this account via SIP. Sending still has no
working path here until Anveo activates HTTP SMS-API access.

Also updates docs/anveo-direct-setup-guide.md's SMS section, which still
described the ntfy-based mechanism this session fully replaced with
AMI/Sipnetic delivery, and corrects its stale "not available on Direct"
sending claim with what's actually been confirmed: SIP MESSAGE outbound
is a dead end (501), the HTTP API is real but pending Anveo activation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 13:41:31 +00:00
Claude 0135b2ed27 Use Destination, not To, for MessageSend's endpoint resolution
`manager show command MessageSend` (this box's own Asterisk, requested
live) documents Destination as the field that actually resolves an
outgoing message's endpoint/technology; To is documented as a
backward-compatible fallback for the destination when Destination is
omitted, and separately as just the outgoing SIP MESSAGE's To: header
content when Destination IS provided. Two live attempts using only To
(bare "pjsip:212", then domain-qualified "pjsip:212@domain") both
produced zero SIP wire traffic -- confirmed via `pjsip set logger on`
during a real delivery attempt against an actively-registered contact --
meaning that documented fallback path isn't actually wired up on this
Asterisk version regardless of what the docs promise.

Switched to Destination using the docs' own "endpoint" form: bare
"pjsip:<ext>", no domain, which resolves via the endpoint's default
aor/contact -- the same live, registered contact `pjsip show contacts`
already confirmed exists for this extension.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 12:45:03 +00:00
Claude 5b339fdc0d Try qualifying MessageSend's To with a domain -- To/From asymmetry
Live test: AMI MessageSend to a bare "pjsip:212" produced zero SIP wire
traffic (confirmed via `pjsip set logger on` during a real delivery
attempt with the target extension actively registered) -- Asterisk
never even tried reaching the registered contact, meaning the failure
was in URI resolution before anything got sent, not a rejection from
the softphone. From already carried a domain (SMS_DOMAIN); To didn't.
Testing whether that asymmetry was the actual cause.

Explicitly a live experiment, not a confirmed fix -- next test will
show whether this produces real SIP MESSAGE traffic in the logger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 04:01:52 +00:00
Claude e48460d6f2 Fix the actual cause of every "file does not exist": ProtectHome=true
Chased this as an ACL-ordering problem for the last several commits, and
those fixes were real and worth keeping, but none of them could ever
have fixed this: ProtectHome=true in the systemd unit doesn't just
restrict permissions, it mounts an empty, invisible filesystem over
/home, /root, and /run/user for the whole unit. ASTERISK_CONFIG_DIR
lives under /root/docker/... (or /home/<user>/docker/... on a non-root
install), so the relay process could never see it regardless of any ACL
grant on the real filesystem underneath -- from inside the sandboxed
unit it genuinely doesn't exist, while a plain unsandboxed shell
(confirmed live: `sudo -u smsrelay cat pstn-personal-dids.conf` outside
systemd) reads the exact same path fine.

Fix: ProtectHome=read-only instead of true. Still stops this service
from writing into /home or /root -- all it should ever need is read --
it just stops hiding them outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:43:38 +00:00
Claude 733a583c6a Grant smsrelay's ACL access after the Asterisk restart, not before
Reproduced live: pstn-personal-dids.conf was confirmed readable by
smsrelay right after a manual ACL grant, then unreadable again
("does not exist" in the relay's log -- os.path.isfile() swallows the
PermissionError and just returns False) immediately after the very next
fresh install. The only thing that ran in between was this same
install's own Asterisk container restart (needed to pick up the new AMI
secret).

The ACL grant was sequenced BEFORE that restart. CLAUDE.md documents the
container's entrypoint re-chowning its mounted config directory on every
restart and says chown alone can't touch ACL entries -- true, but
apparently this image's entrypoint also chmods, and chmod recomputes a
directory's ACL mask entry, which can silently weaken a named-user grant
made before it even though the grant's ACL entry itself is untouched.

Fix: do the grant last, after the restart-or-not branch, so nothing left
in this install run can undo it. ensure_docker_dir_ownership() (chown
only, confirmed in lib/common.sh, no chmod) stays where it was --
chow doesn't need this ordering fix, only the ACL grant does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:27:14 +00:00
Claude e59f16556a Log rejected sms-inbound webhook requests instead of dropping silently
A token mismatch returned a bare 404 with no journal line at all, so
"nothing is happening" was indistinguishable from "no request ever
arrived" -- exactly what a stale provider URL looks like after a
RELAY_TOKEN rotation (every full reinstall generates a new one, which
invalidates whatever's still pasted into the DID's SMS tab until it's
updated). Now logs the request's source IP and path length -- never the
attempted path itself, since that's unauthenticated input from whoever
hit the port, no reason to trust or echo it into the journal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:04:37 +00:00
Claude 9207daa433 Stop the old sms-inbound instance before a fresh reinstall, not after
The port-taken message on the last run ("Port 8093 was taken — the relay
will use 8094") was this service colliding with itself, not a real
conflict. Fresh-install never stopped the previous run before scanning
for a free port, so it always found its own earlier process still bound
to 8093, silently moved to 8094, and then `systemctl enable --now` was a
no-op against an already-active unit -- meaning the OLD process (holding
the OLD AMI secret and OLD relay token, from before this session's
settings.env fix) kept serving traffic while the freshly-written config
and port sat unused underneath it.

Fix: `systemctl stop sms-inbound` right before the port scan. Now the
scan only reports a real conflict from something else, and the box
should settle back on 8093 (or whatever's actually free) with the
process that's really running matching what was just configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 03:00:30 +00:00
Claude 868a903683 Fix sms-inbound crashing on every "update" re-run
Root cause of the "line 9: from: unbound variable" crash: SMS_FORWARD_URL
stores Anveo's own template placeholders literally ($[from]$, $[to]$,
$[message]$ -- text Anveo substitutes on its end, not ours). Written into
settings.env double-quoted, that landed in the file as
SMS_FORWARD_URL="...?from=$[from]$&...". Harmless to write, but the
update path `source`s this same file on every re-run, and bash reads
$[from] as legacy arithmetic expansion ($[...] == $((...))) even inside
double quotes -- a bare name in it means "look up variable from", which
is unset, and setup.sh runs under `set -uo pipefail`, so nounset kills
the whole installer before it gets anywhere near the AMI-diagnostics
code from the last two commits.

Fix: single-quote every value in the written settings.env. A source'd
single-quoted assignment never re-expands its contents, so this is safe
regardless of what SMS_FORWARD_URL (or anything else in that file) holds.

This only fixes future writes -- the box that hit this already has a
broken settings.env on disk from before this fix existed, and "update"
mode sources that file before it gets a chance to rewrite it, so it will
crash the same way one more time even after pulling this. Choosing
"full install" instead on the next run skips the source entirely and
regenerates the file correctly quoted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 02:56:40 +00:00
Claude 3727f375b1 sms-inbound: log exactly why recipient resolution failed
First live test dropped an inbound text with only "no owner found for
this DID, dropped" in the journal -- not enough to tell a config-dir
path problem from an ACL/permission problem from a DID-normalization
mismatch from an actually-unassigned DID, without re-triggering a real
text each time. resolve_recipients() now prints which of those it hit:
config dir unset, file missing (which also silently covers "smsrelay
can't traverse/read config/asterisk" -- os.path.isfile() swallows
PermissionError and just returns False), normalized DID has no section,
section has no owner=, or a group owner has no current members.

Nothing added is more sensitive than what's already visible on the
dashboard itself (config dir path, a DID's own digits, the set of DIDs
that exist) -- never the message body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 02:36:00 +00:00
Claude 34638e8400 Rebuild sms-inbound to deliver texts into Asterisk via AMI, not ntfy
SMS-over-SIP is confirmed unavailable on this DID (the account's SMS tab
only offers "Forward to URL", no MESSAGE/INVITE option) so the diagnostic
dialplan from the previous commit is dead weight for delivery purposes,
though pstn-trunk.sh's message_context wiring stays since it's harmless
and costs nothing to leave in place.

Pivots the whole service: same proven HTTP-webhook front end (rate limit,
constant-time token check, message/from/to extraction), but the delivery
target changes from a push notification to a real SIP MESSAGE landing in
the extension's own softphone (Sipnetic), via Asterisk's Manager
Interface. "Direct mode" and all ntfy options are removed entirely per
instruction to fully replace ntfy, not keep both paths.

Recipient resolution reuses the exact on-disk formats
services/security-dashboard.sh already reads/writes: DID -> owner from
pstn-personal-dids.conf, and for a Ring-Group owner ("@Name"), that
group's current members from pstn-groups.conf -- so delivery always
reflects live DID ownership and live group membership, not a snapshot
from assignment time.

AMI access is scoped tightly: a dedicated "smsrelay" manager.conf user
with only the "message" permission class, bound to 127.0.0.1, secret
generated via generate_password. The service account also gets a POSIX
ACL grant (not chmod/group, which the container's entrypoint reverts on
every restart) for read-only access to the two config files above.

Flagged as UNVERIFIED in code comments and will need a live test: the
"message" AMI permission class name and MessageSend's To/From/Body
parameter names are believed correct from documentation but unconfirmed
against a real Asterisk instance. ami_deliver() logs every AMI response
in full specifically so the first real delivery attempt is
self-diagnosing if something here is wrong.

Verified before commit: bash -n on the full file, and py_compile on the
extracted relay.py (configparser/socket/hmac/http.server, all stdlib).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
2026-07-27 01:58:54 +00:00
Claude 0eb09eec9c Match the real Anveo SMS tab, and document provider-continuity steps
The SMS tab has one control — a "Forward to URL" checkbox and field, with
SAVE/RETURN buttons where RETURN discards. The instructions described a
destination dropdown that isn't there. Also flags that the generated URLs are
long (~90 chars relay, ~150+ direct) and worth re-opening the tab to confirm
they saved whole.

Adds a provider-risk section: the realistic way to lose the number is account
action or a lapsed balance rather than the company folding, so keep the
balance small, save a recent invoice offline (porting out needs a signed LOA
plus the latest bill, which you can't download once an account is closed),
and note that Anveo states it does not block port-outs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-25 09:57:48 +00:00
Claude e1c3203d88 Add sms-inbound: verification codes from a VoIP DID to ntfy push
New service for one narrow job — getting SMS verification codes sent to a
VoIP number onto a phone with no SIM. Deliberately not a texting app: no
outbound path (Anveo Direct has none; that needs an Anveo Retail account, and
a free texting app covers sending), and messages arrive as push notifications
rather than being routed into Asterisk as SIP MESSAGE, since a code you read
and type is better served by a notification than a softphone chat thread.

Two modes, both driven entirely from the provider's "forward SMS to URL" box:

- direct — the provider calls ntfy itself; nothing installed here. ntfy
  accepts GET publishing at /{topic}/(publish|send|trigger) with message and
  title as query params, and auth via ?auth= holding base64url (unpadded) of
  the literal "Bearer <token>" — confirmed against ntfy's server.go and
  server_auth.go rather than its docs.
- relay — a stdlib systemd service, Caddy-fronted on its own domain with no
  Authelia (the provider can't log in; a random 32-char token in the path is
  the secret). Buys two things direct mode can't have: an unescaped "&" in a
  message body survives intact, because the relay takes everything after the
  last message= verbatim instead of parse_qs — which is why the generated URL
  always puts the message placeholder last — and no ntfy credentials sit in a
  third party's web portal.

Verification codes are bearer credentials, so: a 24-char random topic name
(the repo's ntfy defaults to auth-default-access: read-write, making the topic
name the read credential), constant-time token compare, a 60/min rate limit,
and the relay logs sender/recipient/length but never the message body.

The Anveo guide gains a section covering the two things that actually decide
whether codes arrive: short-code support (Anveo has it, unusually — VoIP.ms
does not except for Google) and Anveo's carrier-sourced *mobile* DIDs, which
are classified as mobile in the lookups that reject VoIP numbers at signup.
Also documents MMS and group texts being out of reach, and why the native
Messages app never sees any of this.

Verified against a stub ntfy: plain OTP, encoded "&", unencoded "&", "+" as
space, wrong token (404), missing message (400) and the rate limit (57x204
then 429) all behave; both installer modes were run end to end in a sandbox
and their generated URLs, settings files and READMEs checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
2026-07-25 02:13:39 +00:00