cpanel-doctor 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Tooling
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .mypy_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Editors / OS
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ .DS_Store
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to **cPanel Doctor** are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/).
5
+
6
+ ## [0.2.0] — 2026-06-26
7
+ ### Added
8
+ - `account-startdate` patch — fixes newly created cPanel accounts being recorded
9
+ with a creation date in the **past** (`STARTDATE` in `/var/cpanel/users/<user>`,
10
+ surfaced as `unix_startdate`) even though the OS clock is correct. Installs a
11
+ `Whostmgr::Accounts::Create` (post) hook that rewrites `STARTDATE` to the
12
+ authoritative system date (read via `/usr/bin/date` in a clean child process)
13
+ through `Cpanel::Config::CpUserGuard`, updating both the datastore and the
14
+ `users.cache`. Components `hook_script` and `hook_registration`; self-heals via
15
+ `reapply` / the post-upcp hook if either is dropped.
16
+
17
+ ## [0.1.0] — 2026-06-25
18
+ ### Added
19
+ - Patch engine: component-based patches with applicable / not-applied / **drifted**
20
+ / applied state detection, idempotent apply and clean reverse-removal.
21
+ - `pg-cpses` patch — fixes phpPgAdmin/PostgreSQL "Authentication failed" caused by
22
+ a broken vendor `pam_cpses.so`, without weakening per-account isolation.
23
+ - `https-redirect-date` patch — fixes the greyed-out **Force HTTPS Redirect** toggle
24
+ in cPanel » Domains when the SSL-validity check (`ssl_call.pm`) perceives a past
25
+ date and treats valid certificates as not-yet-valid; the check is made to read the
26
+ authoritative system date instead.
27
+ - Interactive **Textual** TUI and a scriptable CLI (`list`, `status`, `apply`,
28
+ `remove`, `reapply`, `test`, `hook`), with `--dry-run`.
29
+ - Post-`upcp` self-heal hook (`hook install`) that re-applies drifted patches
30
+ after cPanel updates.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 crocky.host
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ # Patches
2
+
3
+ Every fix in cPanel Doctor is a **patch** built from independent **components**
4
+ (a file, a service, a config edit, a hook …). The doctor derives each patch's
5
+ status from its components, so *applicable / not-applied / **drifted** / applied*
6
+ detection, idempotent apply, surgical re-apply and clean removal all come for free.
7
+
8
+ | ID | Fixes |
9
+ |----|-------|
10
+ | [`account-startdate`](#account-startdate--new-accounts-get-a-past-creation-date) | new accounts recorded with a creation date in the past |
11
+ | [`https-redirect-date`](#https-redirect-date--force-https-redirect-greyed-out) | greyed-out **Force HTTPS Redirect** toggle |
12
+ | [`pg-cpses`](#pg-cpses--phppgadmin--postgresql-cpses-login) | phpPgAdmin *"Authentication failed"* (broken `pam_cpses.so`) |
13
+
14
+ ---
15
+
16
+ ## `account-startdate` — new accounts get a past creation date
17
+ When an account is created in WHM, its creation date (`STARTDATE` in
18
+ `/var/cpanel/users/<user>`, shown as `unix_startdate` by `whmapi1 listaccts`) is
19
+ written with a timestamp in the **past**, even though the host's OS clock is
20
+ correct — so every freshly created account shows a wrong creation date in WHM.
21
+ The patch installs a `Whostmgr::Accounts::Create` **post** hook that, right after
22
+ creation, rewrites `STARTDATE` to the **authoritative system date** (`/usr/bin/date`
23
+ in a clean child process, bypassing the bad in-process clock) via
24
+ `Cpanel::Config::CpUserGuard`, updating both the datastore and the `users.cache`
25
+ entry. It only corrects a date that is clearly wrong (off by more than a day) and
26
+ always reports success, so it can never block account creation.
27
+
28
+ Components `hook_script` (the Perl hook on disk) and `hook_registration` (the
29
+ `manage_hooks` entry). It fixes accounts created **after** it is applied; it does
30
+ not rewrite pre-existing dates. Neither component is a vendor file, but if either
31
+ goes missing it reads as **DRIFTED** and is healed by `reapply` / the post-upcp hook.
32
+
33
+ ## `https-redirect-date` — Force HTTPS Redirect greyed out
34
+ The **Force HTTPS Redirect** toggle in *cPanel » Domains* is greyed out even
35
+ though AutoSSL issues valid certificates. cPanel only enables it when it judges
36
+ the certificate *currently valid*; that check (`_ssl_actually_valid` in
37
+ `Cpanel/Admin/Modules/Cpanel/ssl_call.pm`) compares the **process clock** against
38
+ the cert's `not_before`/`not_after`. When this process perceives a date in the
39
+ **past**, valid certs read as "not yet valid", `ssl_valid` is false, and the UI
40
+ disables the toggle (AutoSSL is unaffected). The patch makes that check read the
41
+ **authoritative system date** (`/usr/bin/date` in a clean child process), falling
42
+ back to `time()` if the clock can't be read.
43
+
44
+ Single component `ssl_call`: the vendor file is edited in place (original backed
45
+ up to `ssl_call.pm.orig`). A cPanel update overwrites it → the patch reads as
46
+ **NOT APPLIED** and is restored by `reapply` / the post-upcp hook.
47
+
48
+ ## `pg-cpses` — phpPgAdmin / PostgreSQL cpses login
49
+ On affected builds the vendor `pam_cpses.so` rejects **every** valid PostgreSQL
50
+ cpses session, so phpPgAdmin shows *"Authentication failed"* for all accounts
51
+ (MySQL/phpMyAdmin are fine). The patch routes the `postgresql_cpses` PAM service
52
+ through a validator that accepts only **recent, genuine** cpses session secrets,
53
+ and points phpPgAdmin at the cpses PAM IP (`127.0.0.200`).
54
+
55
+ **Security:** `pg_hba`'s `samerole` is left untouched (per-account DB isolation is
56
+ preserved); the path is loopback-only; the 32-char session secret lives in
57
+ `/var/cpanel/cpses/keys` (`root:cpses 0750`) and is unreadable by ordinary users,
58
+ so it can't be forged. Components: `validator`, `pam` (backs up the original),
59
+ `loopback`, `phppgadmin`.
60
+
61
+ ---
62
+
63
+ ## Writing a new patch
64
+ Drop a `Patch` subclass under `cpanel_doctor/patches/`; it's auto-discovered. A
65
+ patch is a list of `Component`s, each with `present` / `apply` / `remove`. Because
66
+ status is derived from the components, drift detection and surgical re-apply are
67
+ automatic.
68
+
69
+ ```python
70
+ from cpanel_doctor.core import Component, Patch, Runner
71
+
72
+ class MyPatch(Patch):
73
+ id = "my-fix"
74
+ name = "Short title"
75
+ summary = "One line shown in `list`."
76
+ description = "Longer text shown in the TUI detail pane."
77
+
78
+ def applicable(self, r: Runner):
79
+ return r.exists("/some/marker"), "why not, if not"
80
+
81
+ def components(self):
82
+ return [
83
+ Component(
84
+ key="thing",
85
+ description="what it is",
86
+ present=lambda r: r.exists("/etc/thing"),
87
+ apply=lambda r: r.write("/etc/thing", "...", 0o644),
88
+ remove=lambda r: r.remove("/etc/thing"),
89
+ ),
90
+ ]
91
+
92
+ def self_test(self, r: Runner): # optional functional check after apply
93
+ return True, "ok"
94
+ ```
95
+
96
+ Guidelines:
97
+
98
+ * **Idempotent** — `present` must be cheap and side-effect free; `apply` only
99
+ touches what's missing.
100
+ * **Reversible** — `remove` undoes `apply`; back up vendor files (`Runner.backup`)
101
+ so removal can restore them.
102
+ * **Honest applicability** — return `NOT_APPLICABLE` rather than editing a file you
103
+ don't recognise (see `https-redirect-date`, which refuses an unfamiliar build).
104
+ * **Never break the host** — a hook/patch should fail safe (always report success
105
+ to cPanel where relevant) and prefer correcting only clearly-wrong state.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: cpanel-doctor
3
+ Version: 0.2.0
4
+ Summary: Intelligent, interactive doctor that diagnoses, applies and removes fixes (patches) for known cPanel/WHM problems.
5
+ Project-URL: Homepage, https://github.com/CrockyHost/cpanel-doctor
6
+ Project-URL: Issues, https://github.com/CrockyHost/cpanel-doctor/issues
7
+ Author: crocky.host
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cpanel,patch,phppgadmin,postgresql,sysadmin,tui,whm
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Systems Administration
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: rich>=13.0
19
+ Requires-Dist: textual>=0.40
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == 'dev'
22
+ Requires-Dist: ruff>=0.1; extra == 'dev'
23
+ Requires-Dist: textual-dev>=1.2; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # cPanel Doctor
27
+
28
+ **Diagnose and fix known cPanel/WHM problems — intelligently, interactively, reversibly.**
29
+
30
+ cPanel Doctor models each fix as a **patch** made of independent **components**. It
31
+ *knows* whether a patch is applicable, fully applied, not applied, or **drifted**
32
+ (partly reset — e.g. by a cPanel update), and it can apply, re-apply only the
33
+ drifted bits, or cleanly remove a patch. There's a colourful [Textual](https://textual.textualize.io/)
34
+ TUI and a scriptable CLI.
35
+
36
+ ```
37
+ ┌ cPanel Doctor v0.2.0 ──────────────────────────────────────────────┐
38
+ │ ID Status Patch │
39
+ │ account-startdate APPLIED New accounts' creation date (past) │
40
+ │ https-redirect-date APPLIED Force HTTPS Redirect (past date) │
41
+ │ pg-cpses APPLIED phpPgAdmin / PostgreSQL cpses login │
42
+ │ post-upcp self-heal hook: installed (System::upcp post) │
43
+ └─────────────────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pipx install cpanel-doctor # recommended
50
+ # or
51
+ pip install cpanel-doctor
52
+ ```
53
+
54
+ Requires **Python 3.8+** (cPanel's system Python may be older — use `pipx`,
55
+ a venv, or your distro's newer Python). Most actions modify system files and
56
+ need **root**.
57
+
58
+ ## Use
59
+
60
+ ```bash
61
+ cpanel-doctor # interactive TUI (default)
62
+ cpanel-doctor list # one-line status per patch
63
+ cpanel-doctor status pg-cpses # detailed, per-component status
64
+ sudo cpanel-doctor apply pg-cpses
65
+ sudo cpanel-doctor apply pg-cpses --dry-run # preview, change nothing
66
+ sudo cpanel-doctor remove pg-cpses
67
+ cpanel-doctor test pg-cpses # functional self-test
68
+ sudo cpanel-doctor hook install # self-heal after cPanel updates
69
+ ```
70
+
71
+ ### TUI keys
72
+ `a` apply · `r` remove · `h` re-apply drift · `t` self-test · `k` toggle hook ·
73
+ `d` refresh · `q` quit.
74
+
75
+ ## Self-healing after `upcp`
76
+
77
+ cPanel updates overwrite vendor-managed files (for `pg-cpses`, phpPgAdmin's
78
+ `config.inc.php`/`intro.php`), which **drifts** a patch. Install the hook once:
79
+
80
+ ```bash
81
+ sudo cpanel-doctor hook install
82
+ ```
83
+
84
+ It registers a cPanel Standardized Hook on `System::upcp` (post stage) that runs
85
+ `cpanel-doctor reapply` after every update, healing only the drifted components.
86
+
87
+ ## Patches
88
+
89
+ Three patches ship today — see **[PATCHES.md](PATCHES.md)** for full descriptions,
90
+ security notes and a guide to writing your own.
91
+
92
+ | ID | Fixes |
93
+ |----|-------|
94
+ | `account-startdate` | new accounts recorded with a creation date in the past |
95
+ | `https-redirect-date` | greyed-out **Force HTTPS Redirect** toggle |
96
+ | `pg-cpses` | phpPgAdmin *"Authentication failed"* (broken `pam_cpses.so`) |
97
+
98
+ New patches are auto-discovered — drop a `Patch` subclass in
99
+ `cpanel_doctor/patches/` (see [PATCHES.md](PATCHES.md#writing-a-new-patch)).
100
+
101
+ ## Safety
102
+
103
+ - `--dry-run` previews every action without touching the system.
104
+ - `remove` reverses each component (restoring `.orig` backups where taken).
105
+ - Read-only `status`/`list`/`test` never change anything and don't need root.
106
+
107
+ ## License
108
+
109
+ MIT © crocky.host
@@ -0,0 +1,84 @@
1
+ # cPanel Doctor
2
+
3
+ **Diagnose and fix known cPanel/WHM problems — intelligently, interactively, reversibly.**
4
+
5
+ cPanel Doctor models each fix as a **patch** made of independent **components**. It
6
+ *knows* whether a patch is applicable, fully applied, not applied, or **drifted**
7
+ (partly reset — e.g. by a cPanel update), and it can apply, re-apply only the
8
+ drifted bits, or cleanly remove a patch. There's a colourful [Textual](https://textual.textualize.io/)
9
+ TUI and a scriptable CLI.
10
+
11
+ ```
12
+ ┌ cPanel Doctor v0.2.0 ──────────────────────────────────────────────┐
13
+ │ ID Status Patch │
14
+ │ account-startdate APPLIED New accounts' creation date (past) │
15
+ │ https-redirect-date APPLIED Force HTTPS Redirect (past date) │
16
+ │ pg-cpses APPLIED phpPgAdmin / PostgreSQL cpses login │
17
+ │ post-upcp self-heal hook: installed (System::upcp post) │
18
+ └─────────────────────────────────────────────────────────────────────┘
19
+ ```
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pipx install cpanel-doctor # recommended
25
+ # or
26
+ pip install cpanel-doctor
27
+ ```
28
+
29
+ Requires **Python 3.8+** (cPanel's system Python may be older — use `pipx`,
30
+ a venv, or your distro's newer Python). Most actions modify system files and
31
+ need **root**.
32
+
33
+ ## Use
34
+
35
+ ```bash
36
+ cpanel-doctor # interactive TUI (default)
37
+ cpanel-doctor list # one-line status per patch
38
+ cpanel-doctor status pg-cpses # detailed, per-component status
39
+ sudo cpanel-doctor apply pg-cpses
40
+ sudo cpanel-doctor apply pg-cpses --dry-run # preview, change nothing
41
+ sudo cpanel-doctor remove pg-cpses
42
+ cpanel-doctor test pg-cpses # functional self-test
43
+ sudo cpanel-doctor hook install # self-heal after cPanel updates
44
+ ```
45
+
46
+ ### TUI keys
47
+ `a` apply · `r` remove · `h` re-apply drift · `t` self-test · `k` toggle hook ·
48
+ `d` refresh · `q` quit.
49
+
50
+ ## Self-healing after `upcp`
51
+
52
+ cPanel updates overwrite vendor-managed files (for `pg-cpses`, phpPgAdmin's
53
+ `config.inc.php`/`intro.php`), which **drifts** a patch. Install the hook once:
54
+
55
+ ```bash
56
+ sudo cpanel-doctor hook install
57
+ ```
58
+
59
+ It registers a cPanel Standardized Hook on `System::upcp` (post stage) that runs
60
+ `cpanel-doctor reapply` after every update, healing only the drifted components.
61
+
62
+ ## Patches
63
+
64
+ Three patches ship today — see **[PATCHES.md](PATCHES.md)** for full descriptions,
65
+ security notes and a guide to writing your own.
66
+
67
+ | ID | Fixes |
68
+ |----|-------|
69
+ | `account-startdate` | new accounts recorded with a creation date in the past |
70
+ | `https-redirect-date` | greyed-out **Force HTTPS Redirect** toggle |
71
+ | `pg-cpses` | phpPgAdmin *"Authentication failed"* (broken `pam_cpses.so`) |
72
+
73
+ New patches are auto-discovered — drop a `Patch` subclass in
74
+ `cpanel_doctor/patches/` (see [PATCHES.md](PATCHES.md#writing-a-new-patch)).
75
+
76
+ ## Safety
77
+
78
+ - `--dry-run` previews every action without touching the system.
79
+ - `remove` reverses each component (restoring `.orig` backups where taken).
80
+ - Read-only `status`/`list`/`test` never change anything and don't need root.
81
+
82
+ ## License
83
+
84
+ MIT © crocky.host
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cpanel-doctor"
7
+ version = "0.2.0"
8
+ description = "Intelligent, interactive doctor that diagnoses, applies and removes fixes (patches) for known cPanel/WHM problems."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "crocky.host" }]
13
+ keywords = ["cpanel", "whm", "postgresql", "phppgadmin", "patch", "sysadmin", "tui"]
14
+ classifiers = [
15
+ "Environment :: Console",
16
+ "Intended Audience :: System Administrators",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: System :: Systems Administration",
21
+ ]
22
+ dependencies = [
23
+ "textual>=0.40",
24
+ "rich>=13.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=7", "textual-dev>=1.2", "ruff>=0.1"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/CrockyHost/cpanel-doctor"
32
+ Issues = "https://github.com/CrockyHost/cpanel-doctor/issues"
33
+
34
+ [project.scripts]
35
+ cpanel-doctor = "cpanel_doctor.cli:main"
36
+ cpdoctor = "cpanel_doctor.cli:main"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ # Assets (.sh/.service/.pl) live inside the package and are included automatically.
40
+ packages = ["src/cpanel_doctor"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/cpanel_doctor", "README.md", "LICENSE", "PATCHES.md", "CHANGELOG.md"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py38"
@@ -0,0 +1,5 @@
1
+ """cPanel Doctor — diagnose and fix known cPanel/WHM problems, intelligently."""
2
+ from __future__ import annotations
3
+
4
+ __version__ = "0.2.0"
5
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from cpanel_doctor.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,96 @@
1
+ #!/usr/local/cpanel/3rdparty/bin/perl
2
+
3
+ # cpanel-doctor:account-startdate
4
+ # ------------------------------------------------------------------------------
5
+ # cPanel Doctor :: account creation-date fix
6
+ #
7
+ # Registered as a cPanel Standardized Hook on Whostmgr::Accounts::Create (post
8
+ # stage). On affected hosts the account creation date (STARTDATE in
9
+ # /var/cpanel/users/<user>) is written with a timestamp in the *past* at
10
+ # creation time, even though the OS clock is correct -- so freshly created
11
+ # accounts show a wrong creation date in WHM. This hook runs right after an
12
+ # account is created and rewrites STARTDATE to the real "now".
13
+ #
14
+ # The true current time is read from a CLEAN child process (env -i + /bin/date),
15
+ # bypassing any per-process time override (e.g. an LD_PRELOAD/libfaketime
16
+ # inherited from the account-creation process): a plain `date` on the host is
17
+ # correct, so dropping the inherited environment is enough to get the real
18
+ # epoch. Falls back to the in-process time() if the clock can't be read.
19
+ #
20
+ # Writing through Cpanel::Config::CpUserGuard updates BOTH the datastore
21
+ # (/var/cpanel/users/<user>) and the cache (/var/cpanel/users.cache/<user>).
22
+ #
23
+ # Installed / removed automatically by: cpanel-doctor apply|remove account-startdate
24
+ # Managed by cpanel-doctor -- do not edit by hand.
25
+ #
26
+ # Fail-safe: any problem is logged and the hook still reports success (post
27
+ # stage; it must never disrupt account creation).
28
+
29
+ use strict;
30
+ use warnings;
31
+
32
+ use Cpanel::JSON ();
33
+ use Cpanel::Config::CpUserGuard ();
34
+
35
+ my $LOG = '/var/log/cpanel-doctor.log';
36
+
37
+ # Best-effort log line; never fatal.
38
+ sub _log {
39
+ my ($msg) = @_;
40
+ eval {
41
+ open my $fh, '>>', $LOG or return;
42
+ my $ts = localtime();
43
+ print {$fh} "$ts account-startdate: $msg\n";
44
+ close $fh;
45
+ };
46
+ return;
47
+ }
48
+
49
+ # Always report success to cPanel (first line "1 <message>"), then exit.
50
+ my $done = sub {
51
+ my ($msg) = @_;
52
+ _log($msg);
53
+ print "1 account-startdate: $msg\n";
54
+ exit 0;
55
+ };
56
+
57
+ # ---- read the hook payload (JSON on STDIN) ---------------------------------
58
+ my $raw = do { local $/; <STDIN> };
59
+ my $payload = eval { Cpanel::JSON::Load($raw) } || {};
60
+
61
+ # Accounts::Create delivers the new account name under data.user.
62
+ my $user =
63
+ $payload->{'data'}{'user'}
64
+ // $payload->{'data'}{'username'}
65
+ // $payload->{'user'};
66
+
67
+ $done->("no user in payload, nothing to do") if !defined $user || $user eq '';
68
+
69
+ $user =~ s/[^A-Za-z0-9._-]//g; # sanitize
70
+ $done->("invalid user name, skipping") if $user eq '';
71
+ $done->("no datastore for '$user', skipping") if !-e "/var/cpanel/users/$user";
72
+
73
+ # ---- true current epoch, bypassing any inherited time faking ---------------
74
+ my $now = '';
75
+ {
76
+ my $out = `/usr/bin/env -i /bin/date +%s 2>/dev/null`;
77
+ ($now) = ( ( $out // '' ) =~ /(\d{9,})/ );
78
+ }
79
+ $now ||= time(); # last-resort fallback
80
+
81
+ # ---- correct STARTDATE only if it is clearly wrong (off by > 1 day) --------
82
+ my $guard = eval { Cpanel::Config::CpUserGuard->new($user) }
83
+ or $done->("could not open datastore for '$user'");
84
+
85
+ my $cur = $guard->{'data'}{'STARTDATE'} // 0;
86
+
87
+ if ( abs( $now - $cur ) <= 86400 ) {
88
+ $done->("STARTDATE for '$user' already current ($cur), leaving as-is");
89
+ }
90
+
91
+ $guard->{'data'}{'STARTDATE'} = $now;
92
+
93
+ eval { $guard->save(); 1 }
94
+ or $done->("save failed for '$user': $@");
95
+
96
+ $done->("STARTDATE for '$user' corrected: $cur -> $now");
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ #
3
+ # cPanel Doctor :: post-upcp hook
4
+ # ------------------------------------------------------------------------------
5
+ # Registered as a cPanel Standardized Hook on System::upcp (post stage). cPanel
6
+ # updates overwrite vendor-managed files (e.g. phpPgAdmin's config.inc.php), which
7
+ # silently un-does parts of some patches. After every upcp this re-applies any
8
+ # DRIFTED cpanel-doctor patch -- surgically, only the missing pieces.
9
+ #
10
+ # Registered/removed automatically by: cpanel-doctor hook install | hook remove
11
+ #
12
+ # cPanel passes the hook context as JSON on stdin; we ignore it and just heal.
13
+ cat >/dev/null 2>&1 # drain stdin
14
+
15
+ LOG=/var/log/cpanel-doctor.log
16
+ {
17
+ echo "=== $(date '+%F %T') post-upcp: re-applying drifted patches ==="
18
+ # Prefer the installed console script; fall back to module execution.
19
+ if command -v cpanel-doctor >/dev/null 2>&1; then
20
+ cpanel-doctor reapply --yes
21
+ elif command -v cpdoctor >/dev/null 2>&1; then
22
+ cpdoctor reapply --yes
23
+ else
24
+ "${CPANEL_DOCTOR_PYTHON:-python3}" -m cpanel_doctor reapply --yes
25
+ fi
26
+ } >>"$LOG" 2>&1
27
+
28
+ # Standardized hooks must print a JSON result and exit 0.
29
+ echo '{"status":1,"message":"cpanel-doctor reapply complete"}'
30
+ exit 0
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=cPanel Doctor: cpses loopback 127.0.0.200 for phpPgAdmin/PostgreSQL
3
+ Documentation=https://github.com/CrockyHost/cpanel-doctor
4
+ After=network-pre.target
5
+ Before=postgresql.service
6
+ DefaultDependencies=no
7
+
8
+ [Service]
9
+ Type=oneshot
10
+ RemainAfterExit=yes
11
+ ExecStart=/bin/bash -c '/sbin/ip addr show dev lo | grep -qw 127.0.0.200 || /sbin/ip addr add 127.0.0.200/32 dev lo'
12
+ ExecStop=-/sbin/ip addr del 127.0.0.200/32 dev lo
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ #
3
+ # cPanel Doctor :: cpses PostgreSQL auth validator
4
+ # ------------------------------------------------------------------------------
5
+ # Drop-in replacement for the stock (broken) pam_cpses.so, wired ONLY into the
6
+ # `postgresql_cpses` PAM service -- the cpses path on 127.0.0.200 that phpPgAdmin
7
+ # uses. On affected cPanel builds the vendor module rejects every valid cpses
8
+ # session, breaking phpPgAdmin/PostgreSQL for all accounts. This restores access
9
+ # WITHOUT weakening per-account isolation:
10
+ #
11
+ # * pg_hba's `samerole` restriction is UNCHANGED -> each cPanel user can still
12
+ # only reach databases of roles it belongs to (its own databases).
13
+ # * Reachable only from source 127.0.0.200 (loopback); never exposed externally.
14
+ # * A login as user X is accepted ONLY if the supplied credential proves
15
+ # knowledge of a RECENT (<= WINDOW s) cpses session secret for user X. The
16
+ # key files live in /var/cpanel/cpses/keys (root:cpses 0750) and are
17
+ # unreadable by ordinary cPanel users, so the secret cannot be forged.
18
+ # * The secret (otp) is 32 random characters, regenerated per session.
19
+ #
20
+ # Managed by cpanel-doctor (patch id: pg-cpses). Do not edit by hand.
21
+ #
22
+ exec 2>/dev/null
23
+ KEYDIR=/var/cpanel/cpses/keys
24
+ WINDOW=300 # seconds a session key remains valid
25
+
26
+ user="$PAM_USER"
27
+ pass="$(cat)"
28
+ pass="${pass%$'\n'}"
29
+
30
+ [ -z "$user" ] && { logger -t cpses_pg "deny: empty user"; exit 1; }
31
+ [ -z "$pass" ] && { logger -t cpses_pg "deny user=$user: empty pass"; exit 1; }
32
+
33
+ now=$(date +%s)
34
+ rc=1
35
+ shopt -s nullglob
36
+ for kf in "$KEYDIR/$user:cpses_"*; do
37
+ [ -f "$kf" ] || continue
38
+ mt=$(stat -c %Y "$kf" 2>/dev/null) || continue
39
+ [ $(( now - mt )) -gt "$WINDOW" ] && continue
40
+ otp="$(cat "$kf" 2>/dev/null)"
41
+ tempuser="${kf##*:}" # cpses_<2 acct chars><8 random>
42
+ [ -z "$otp" ] && continue
43
+
44
+ # cPanel's credential format is: <tempuser><1-char separator><otp>
45
+ # Verified exact: starts with tempuser, ends with the 32-char otp, exactly
46
+ # one separator char between -> requires knowledge of BOTH session secrets.
47
+ tl=${#tempuser}; ol=${#otp}
48
+ if [ "${#pass}" -eq $(( tl + 1 + ol )) ] \
49
+ && [ "${pass:0:tl}" = "$tempuser" ] \
50
+ && [ "${pass:$(( ${#pass} - ol ))}" = "$otp" ]; then rc=0; break; fi
51
+
52
+ # exact fallbacks for other possible cPanel credential formats
53
+ [ "$pass" = "$otp" ] && { rc=0; break; }
54
+ [ "$pass" = "$tempuser" ] && { rc=0; break; }
55
+ [ "$pass" = "${tempuser}${otp}" ] && { rc=0; break; }
56
+ done
57
+
58
+ logger -t cpses_pg "user=$user result=$([ $rc -eq 0 ] && echo ALLOW || echo DENY)"
59
+ exit $rc