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.
- cpanel_doctor-0.2.0/.gitignore +23 -0
- cpanel_doctor-0.2.0/CHANGELOG.md +30 -0
- cpanel_doctor-0.2.0/LICENSE +21 -0
- cpanel_doctor-0.2.0/PATCHES.md +105 -0
- cpanel_doctor-0.2.0/PKG-INFO +109 -0
- cpanel_doctor-0.2.0/README.md +84 -0
- cpanel_doctor-0.2.0/pyproject.toml +47 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/__init__.py +5 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/__main__.py +4 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/assets/cpanel_doctor_account_startdate.pl +96 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/assets/cpanel_doctor_posthook.sh +30 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/assets/cpses-postgres-ip.service +15 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/assets/pam_cpses_postgres.sh +59 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/cli.py +235 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/core/__init__.py +35 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/core/patch.py +127 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/core/registry.py +32 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/core/runner.py +119 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/hook.py +49 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/patches/__init__.py +1 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/patches/account_startdate.py +166 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/patches/https_redirect_date.py +212 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/patches/pg_cpses.py +203 -0
- cpanel_doctor-0.2.0/src/cpanel_doctor/tui.py +230 -0
|
@@ -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,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
|