Repository navigation
Read the cPanel hook payload where cPanel puts it - #90
Conversation
Creating an account on the live box produced nothing: no queue entry, no log
line, not even an error. The zone existed in PowerDNS and the hook was
registered on Accounts::Create at stage post, pointing at a script that worked
when run by hand.
The script reads stdin and looks for "domain" at the top level. cPanel sends:
{"context":{"category":"Whostmgr","event":"Accounts::Create","stage":"post"},
"hook":{...},
"data":{"user":"acct","domain":"second555domain.org", ...}}
The name is under "data". Finding nothing, the hook exits 0 without logging —
so account creation on cPanel has never reached SecondDNS, silently. The same
shape applies to the delete hook and to addon domains.
That also explains the installer's own account sync removed in #88: with the
hooks not firing, something had to register the zones.
Both hooks now read "data" when it is an object and fall back to the top level.
tests/test_cpanel_hook_payload.sh extracts the parser from each hook — so the
test cannot drift from the code — and feeds it the real WHM payload, the flat
shape, a payload with no domain, malformed input, and an addon domain arriving
as newdomain. Mutations: reading the top level fails 2, dropping newdomain
fails 1.
Two more, both found on the live box after the payload fix.
Accounts::Remove carries only the user:
"data":{"killdns":1,"user":"second444mydom","keep_remote_databases":0}
There is no domain to read, so deleting an account removed nothing. The
installer already registers this at stage pre — the comment says "domain data
available before account is gone" — but the hook never used it. It now reads
userdata/<user>/main and queues every domain of the account: main, addon and
parked alike. Parsed by hand rather than through PyYAML, which is not a
dependency anywhere else.
And addon domains never fired at all: the installer registered
Api2::AddonDomain::addaddon, an event current cPanel does not have. Addon,
parked and alias domains all go through Whostmgr::Domain::park / ::unpark,
which carry target_domain (the account's main domain) and new_domain (the one
being added). Reading "domain" would have registered the wrong name. Both
events are now registered; the Api2 pair stays as a fallback for older cPanel.
Live, deleting an account with three domains:
10:13:59 Zone deleted: addon4444domain.info (addon)
10:13:59 Zone deleted: second777domain.com (main)
10:13:59 Zone deleted: second1123domain.net (alias)
10:14:25 all delivered
Tests cover the park payload for both hooks, every domain of an account from
userdata, the DNS= fallback, an unknown user yielding nothing, and an explicit
domain still winning. Mutation: dropping new_domain fails 1 — the hook would
take target_domain, registering the account's main domain in place of the
addon.
|
Two more defects, both found on the live box after the payload fix landed. Same PR, same root cause: nobody had ever watched these hooks run. Deleting an account removed nothing. "data":{"killdns":1,"user":"second444mydom","keep_remote_databases":0}No domain to read. The installer already registers this at stage Addon domains never fired at all. The installer registered "data":{"target_domain":"second777domain.com","new_domain":"addon4444domain.info","user":"acct"}
LiveCreating an alias, then an addon, then deleting the account that held them: Three domains of one account, one hook, one event. Tests13 checks now: the park payload for both hooks, every domain of an account read from userdata, the One observation, not fixed hereThe owner check inside the delete hook reports |
|
One item, then ready. The three fixes are right and the live proof (alias, addon, account with three domains — three zones removed by one hook) is the kind that counts; park/unpark registration with the Api2 events kept as fallback, new_domain over target_domain, and the pre-stage userdata read all check out. The item: in domain_delete.sh the account path fails silently twice — 'except Exception: try … except Exception: pass' and the whole python under 2>/dev/null — so an account whose userdata cannot be read is removed from the panel while every one of its zones stays on the secondary, with no line anywhere. That is the silent-wrong-answer class again. When the list comes back empty for a user (not for a single-domain event), log it with the user name before exiting 0, and add the row: userdata absent → one log line, nothing queued. Then ready. CI green, no attribution trailers, comments within two lines. |
The lookup failed twice in silence: except Exception: pass inside the parser and 2>/dev/null around it. An account whose userdata cannot be read would disappear from the panel with every one of its zones left on the secondary and nothing in the log to say so. With a user in the payload and no domains found, the hook now logs the account name and the path to check, and queues nothing. Mutation: dropping that line fails the test.
|
Done — and you named the shape exactly: two layers of silence, With a user in the payload and no domains found, the hook now writes one line and queues nothing: Verified on the live box with a user that does not exist: the line appears, The test needed |
|
Verified: an account whose domain list comes back empty now logs it by name with the path to check, the row asserts the log line for a missing userdata, CI green, no attribution trailers, comments within two lines. Ready to merge into develop; the rest of the cPanel circle runs on the merged state. |
Creating an account on the live cPanel box produced nothing: no queue entry, no log line, not even an error. The zone existed in PowerDNS, the hook was registered on
Accounts::Createat stagepost, and the script it pointed at worked when run by hand.The script reads stdin and looks for
domainat the top level. This is what cPanel actually sends:{"context":{"category":"Whostmgr","event":"Accounts::Create","stage":"post"}, "hook":{"exectype":"script","hook":"/usr/local/bin/seconddns-cpanel-domain_create.sh"}, "data":{"user":"second555domain","domain":"second555domain.org","plan":"default", ...}}The name is under
data. Finding nothing, the hook doesexit 0without logging — so account creation on cPanel has never reached SecondDNS, and never said so. The delete hook has the same bug, and so does the addon-domain path.This also explains the installer's own account sync that #88 removed: with the hooks silently doing nothing, something had to register the zones.
Fix
Both hooks read
datawhen it is an object, falling back to the top level so any older or different shape keeps working.Tests
tests/test_cpanel_hook_payload.shextracts the parser out of each hook file — so the test cannot drift from the code it checks — and feeds it:datahas no domain (must yield nothing, not invent one)newdomainunderdataMutations: reading the top level fails 2; dropping
newdomainfails 1.Note on how it hid
The silent
exit 0is what made this survive: a hook that cannot find its subject looks exactly like a hook that had nothing to do. Worth a follow-up — a hook that receives a payload it cannot read should say so — but that is a change in behaviour for every panel, so not in this PR.