[Русский](ИЗМЕНЕНИЯ.md) · **English**

# HostinPL 5.6 · What changed compared to the original

Original: **HostinPL 5.6** as a "nulled" build (the `Xopowblu-4EJlOBEK/HostinPL-5.6` fork),
written for Debian 9 and PHP 7.0.

Below is everything we changed, with file names and line numbers. Panel code changes live in `panel/`.

**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**

---

## 1. The panel now runs on modern PHP

The original targets PHP 7.0. On PHP 8 parts of it died outright. It now runs on **PHP 8.4**:
walking all 21 sections (home, servers, news, status, tickets, web hosting and the whole admin area)
returns **200 on every one and zero errors in the log**.

| File | Before | After |
|------|--------|-------|
| `engine/engine_ftp/elFinder.class.php:4497` | `utf8_encode()` — removed in PHP 8.2, the file manager died with a fatal error | Wrapped in `function_exists()`, falls back to `mb_convert_encoding($str,'UTF-8','ISO-8859-1')` |
| `application/views/admin/checksys/index.php:22` | `apache_get_modules()` — only exists under Apache with mod_php, so the "System check" page returned 500 under nginx | Wrapped in `function_exists()`, otherwise checked via `REQUEST_URI` |
| `application/views/admin/index.php:215` | `$item['invoice_ammount']` used after the `foreach`, producing an Undefined variable when there were no invoices | `isset()` check |
| `application/models/users.php:199-206` | `$city[1]`, `$country[1]`, `$countryCode[1]` used without checking that the regex matched | `isset()` checks |
| `application/controllers/common/loginheader.php:35` | `$_GET['ref']` without `isset` | `isset()` plus a cast to `(int)` |

### What turned out to be a false alarm

The bundled **phpseclib 1.x** uses `create_function()`, which PHP 8 removed. It looked like a blocker,
but checking showed that **phpseclib is not included anywhere in the panel** — it is dead code.
Communication with game nodes goes through the native `php-ssh2` extension
(`engine/libs/ssh2.php` → `ssh2_exec`). Nothing needed fixing.

`get_magic_quotes_gpc()` in `elFinderConnector.class.php:320` is equally harmless: it sits behind
`version_compare(PHP_VERSION,'5.4','<') && ...`, so on PHP 8 it is never reached.

---

## 2. The captcha is off and managed from the admin area

The captcha used to be hard-wired: without valid Google keys **it was impossible to log in**,
and the form displayed "ERROR for site owner: Invalid site key". There is now a `captcha_enable`
flag in `application/config.php`, defaulting to `0` (off).

**Backend.** All four validators received the line
`if($this->config->captcha_enable != '1') return $result;` before the captcha check:

* `application/controllers/account/login.php` — login
* `application/controllers/common/loginheader.php` — registration and the contact form (2 places)
* `application/controllers/tickets/create.php` — ticket creation

**Markup.** The five captcha widgets (4 in `views/common/loginheader.php`, 1 in
`views/tickets/create.php`) are wrapped in `<?php if(@$captcha_enable == '1'): ?>`. Google's `api.js`
is only loaded when the captcha is on. Every `grecaptcha.reset(...)` call became
`window.grecaptcha && grecaptcha.reset(...)` — otherwise, with the captcha off, JavaScript threw
inside the error handlers and the forms stopped responding.

**Admin area.** `views/admin/settings.php`, the "Other settings" tab, gained a
"Bot protection (reCAPTCHA v2)" block: an Off/On selector plus Site key and Secret key fields.

Verified both ways: with the captcha off, login and registration succeed and the user is really
created in the database; with it on, a fresh session gets "Confirm that you are not a robot!"
and the widget returns to the page.

> **Careful when adding your own settings.** The settings writer matches configuration lines
> **by substring** (`strpos`). That is why the flag is called `captcha_enable` and not `captcha`:
> the string `captcha` occurs inside `recaptcha` and `secret_recaptcha`, and saving would have
> overwritten the wrong parameter. New keys must not be substrings of existing ones.

---

## 3. Vulnerabilities fixed

In brief (full detail with code in [SECURITY.en.md](SECURITY.en.md)):

* **Two pre-authentication SQL injections** in `application/models/users.php` (`createAuthLog()`):
  the login journal received the password from the form and the IP from the `CF-Connecting-IP`
  header — which was not validated at all — without escaping. Every value now goes through
  `$this->db->escape()` or a cast to `(int)`, and the header is validated with
  `filter_var(..., FILTER_VALIDATE_IP)`.
* **Plaintext passwords**: `login.php` wrote the real password into the `authlog` table on every
  login attempt, including failed ones. Removed.
* **XSS** in the hidden `ref` field of the registration form — `htmlspecialchars()` added.
* The request to the `ip-api.com` geolocation service now only runs for a valid IP and uses `urlencode()`.

---

## 4. The installer was rewritten from scratch

The original `install` was dangerous on any current system:

* `echo "deb ... stretch main" > /etc/apt/sources.list` — **wiped the repository list** and replaced
  it with Debian 9, after which apt was broken
* installed `php7.0` and hard-coded edits to `/etc/php/7.0/apache2/php.ini`
* switched MariaDB to `bind-address = 0.0.0.0` without a word of warning
* only ran when `/etc/issue.net` said `Debian9`, and otherwise refused to start
* generated passwords and tokens but saved them nowhere — you could only copy them off the screen
* carried on after any failure: every command ended in `> /dev/null 2>&1`

The new scripts:

**`install-panel.sh`** — nginx, PHP 8.x (version detected automatically), MariaDB, a database with a
random password, configuration, scheduler, autostart watchdog, administrator creation, and a final
check that the login page really is served with its form. Idempotent: running it again does not wipe
a database that is already loaded. Credentials are written to `/root/.redl-panel-credentials`
(chmod 600). With `set -euo pipefail` the script stops on error instead of pretending all is well.

**`install-node.sh`** — Docker from the official repository, image build, the
`/home/cp/gameservers/files` layout, the `gameservers` group, MariaDB for game server databases,
SteamCMD, ProFTPD, and sshd configuration that rolls back if `sshd -t` fails. **Before doing anything
it checks whether Docker can work on this machine at all** (`unshare -Ur`) and says so plainly if it
cannot. At the end it prints ready-to-use location details and firewall commands.

Neither script touches `/etc/apt/sources.list`.

---

## 5. The game server image was rebuilt

The original `docker/Dockerfile.original-stretch` is based on `debian:stretch`, and the Debian 9
repositories were shut down in 2023 — **that image no longer builds**, `apt-get update` inside it fails.

The new `docker/Dockerfile` is based on Debian 12 (bookworm) but **must still be tagged
`debian:stretch`**: that name is hard-coded in the panel
(`application/models/servers.php:642`, `docker create ... debian:stretch`) and cannot be changed
without editing the panel.

What is inside: 32-bit libraries (SA-MP, CRMP, MTA and older CS builds are i386), `screen` for
consoles, Java for Minecraft, Node.js 20 for RAGE:MP, and `gdb` for crash analysis.
Node.js comes from NodeSource **over HTTPS with repository key verification**, unlike before.

---

## 6. Working without systemd

The panel assumes systemd is present. Container VPSes do not have it, so watchdogs were added on
cron: `/usr/local/bin/hostinpl-guard` (panel) and `/usr/local/bin/gamenode-guard` (node).
Once a minute (every 2 minutes on a node) they check MariaDB, PHP-FPM, nginx, Docker and cron and
restart whatever died, plus an `@reboot` job to bring everything up after a restart.

Panel liveness is determined by an **HTTP request** to the login page rather than by searching for a
process name — a pattern search would have matched the watchdog's own command line.

---

## 7. Scheduler

The original pointed its 9 jobs at the panel's public domain. They now go to `127.0.0.1`
(independent of DNS and external reachability) and carry `-m` timeouts so that a hung request does
not pile up processes.

> **Token pitfall.** You cannot extract the scheduler token from the config with a plain
> `grep "'token'"` — the line `'yk_password1' => 'token'` matches the same pattern, two values end
> up in the URL separated by a newline, and the jobs then fail silently. The installer writes the
> token directly when it generates the configuration.

---

## 8. Interface languages: Russian and English

The original was Russian-only, with every string hard-coded in the templates. The build now has a
translation layer:

* `engine/main/lang.php` — the `Lang` class and the global `t()` helper
* `application/lang/ru.php`, `application/lang/en.php` — dictionaries
* Language selection order: `?lang=` parameter → `lang` cookie → **the browser's `Accept-Language`
  header** → panel default
* A switcher (RU / EN) in the header of the client area and on the login page
* Anything not yet translated falls back to the original Russian text, so nothing can disappear
  from the interface

Translation is applied to the finished response via `ob_start()`, so none of the 200 template files
had to be rewritten and the coverage extends to the admin area and the e-mails at once. Replacement
only happens on word boundaries — without that a short key corrupts longer words ("Мод" turned
"Модуль" into "Modуль").

### Translation coverage

The dictionary holds **1304 phrases** — everything the panel shows a human: the client area,
ordering a server, server management (the console with every RCON command described, FTP, MySQL
databases, the firewall, the task scheduler, mod auto-install), tickets, the whole FAQ, the entire
admin area, the e-mails and the scheduler's status messages.

Coverage is not eyeballed: a script replays the replacement logic against the sources and lists
what would stay Russian. By that measure **four** items are uncovered, and all four should be:

* "Русский" — the label of the language switcher itself;
* `rus => Pyccĸий` and `ukr => Українська` in the FAQ — these are game server language codes, not UI;
* the name validation regular expression `/^([А-ЯЁ])([а-яё]{1,15})$/u` — code, not text.

Two traps that produced mixed-language output were fixed separately:

* the preposition "в" was its own dictionary entry and got substituted inside sentences that were not
  translated yet, producing "at поле имя пользователя". Function words like that are only translated
  together with the phrase they belong to;
* the date format `d.m.Y в H:i` contains the Russian "в" between the date and the time. It now goes
  through `t()` (31 `date()` calls across 20 files), so English mode renders `30.07.2026 14:22`.

Verified by a live crawl of 45 pages in English mode, including the creation forms and the hidden
settings tabs. Details: see [GUIDE.en.md](GUIDE.en.md), the "Interface language" section.

---

## 9. Branding and dates

* The year in footers: `2020©` → `2026©` (`views/common/footer.php`, `views/common/loginheader.php`)
* Name and description are REDL.IO: `application/config.php` (`description`, `keywords`,
  `mail_sender`, `mail_from`), footers, the "System check" page
* Links to the previous build owner's sites (`hostinpl.ru`, `osmp.ga`) replaced with `redl.io`
* "VK community" links pointing at somebody else's community replaced with `redl.io`
  (`footer.php`, `views/main/index.php`, `views/offline/index.php`)
* All 15 e-mail templates: a "REDL.IO / AI-powered hosting" header and the signature
  "Sincerely, the REDL.IO team"

**The authors' copyright headers were not removed** — they remain in every file that had them.

---

## 10. phpMyAdmin

The panel links to `/phpmyadmin` from the admin area. The original installer set phpMyAdmin up
through Apache, which did not work under nginx. It is now installed from the distribution repository
and served by nginx itself.

> **Reverse proxy pitfall.** For the address `/phpmyadmin` (without a trailing slash) nginx replies
> with a redirect and by default puts **its own port** into it, for example
> `http://panel.example.com:8095/phpmyadmin/`. If the panel sits behind a reverse proxy that address
> is unreachable from outside and the admin link does not open. The cure is
> `absolute_redirect off; port_in_redirect off;`, already present in the configuration the installer
> generates.

---

## What we did NOT do

* We did not rewrite password hashing. The panel stores passwords as **unsalted MD5**
  (`md5($password)`, a `varchar(32)` column). Moving to `password_hash()` affects login,
  registration, recovery and password changes, and requires migrating existing users.
* We did not move game server creation away from Docker.
* We did not delete `panel/application/public/js/proxy/proxy.php` — the file is defanged but kept as
  evidence (see [SECURITY.en.md](SECURITY.en.md)).
* We did not test the payment gateways with real payments, nor VK login.
* We did not verify the game servers themselves: that needs a node with Docker.

---

**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
