В этой статье я рассказал как создать приложение на php, она взята за основу и многие моменты второй раз не освещаются.
А здесь рассказано как запустить безопасный сервер с php от root.
Здесь же я решил доработать backend до нормального состояния и упаковать все в установочный пакет. Хотя в первую очередь, было решено переписать хранение данных с MySQL на Firebase, просто потому что она достаточно удовлетворяет условиям проекта и не требует разворачивать еще и сервис баз данных.
Чтобы не геморроиться потом с запуском, создадим сервис. Сервисы в CentOS 7.xx распределены по трем папкам:
/usr/lib/systemd/system/ – сервисы из установленных пакетов rpm.
/run/systemd/system/ — сервисы, созданные в рантайме.
/etc/systemd/system/ — сервисы, созданные администратором, здесь и создадим наш сервис:
1 2 |
vi /etc/systemd/system/php-server.service |
С содержимым:
1 2 3 4 5 6 7 |
[Unit] Description=internal php-root server after=network.target WorkingDirectory=/etc/openvpn/scripts User=root Group=root |
[Service]
ExecStart=/bin/php -S 127.0.0.1:999 -t /etc/openvpn/scripts/
ExecStop=kill -9 $(pidof php -S)
[Install]
WantedBy=multi-user.target
Психанул и решил запилить всю работу через firebase. Т.к. из коробки php с firebase не работает, а серверной части по сути нужно только получить инфу из хранилища — будем работать через REST API.
Для начала попробовал работу в браузере. Для начала побеспокоимся о безопасности и нарисуем права в firebase:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "rules": { ".read": false, ".write": false, "$uid":{ ".read": "auth.uid == $uid", ".write": "auth.uid == $uid" }, ".indexOn": ["adminmail", "server"] } } |
Пример конфигурации базы данных: test-db.json
что означает запрет на чтение-запись всем пользователям во всей базе, кроме как в каталог с собственным uid.
Для авторизованной работы с firebase для начала получим токен авторизации POST-запросом:
1 2 3 4 5 6 7 8 |
https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=токен_проекта Content-Type:application/json { "email": "емайл_пользователя_БД", "password": "пароль_пользователя_БД", "returnSecureToken": true } |
А затем получим данные GET-запросом:
1 2 |
https://assistant-test-aae4e.firebaseio.com/users.json?auth=idToken(из ответа на предыдущий запрос) |
Для всего этого написал три функции, получение токена авторизации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function GetFirebaseUserToken($db_key_this, $db_user_this, $db_pass_this){ $url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key='.$db_key_this; $json = '{"email": "'.$db_user_this.'","password": "'.$db_pass_this.'","returnSecureToken": true}'; $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json',) ); $result = curl_exec($ch); try { $result_arr = json_decode($result, true); return [$result_arr['idToken'], $result_arr['localId']]; } catch(Exception $e) { return 'auth_firebase_error'; } } |
Получение параметров сервера:
1 2 3 4 5 6 7 8 |
function GetServerConf($db_key_this, $db_user_this, $db_pass_this, $id_server_this, &$variable_this){ $user_token = GetFirebaseUserToken($db_key_this, $db_user_this, $db_pass_this); if($user_token == 'auth_firebase_error'){ echo 'Ошибка авторизации в базе данных.'; exit; } $url = 'https://vpnsergdudkotk.firebaseio.com/'.$user_token[1].'.json?auth='.$user_token[0]; |
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, «GET»);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
‘Content-Type: application/json’,)
);
$result = curl_exec($ch);
try {
$result_arr = json_decode($result, true);
$variable_this = $result_arr[‘server’][$id_server_this];
if(isset($variable_this[‘dev’])){
$variable_this[‘Id’] = $id_server_this;
}
} catch(Exception $e) {
}
}
Получение конфигурации исходящей почты:
1 2 3 4 5 6 7 8 |
function GetPostConf($db_key_this, $db_user_this, $db_pass_this, &$variable_this){ $user_token = GetFirebaseUserToken($db_key_this, $db_user_this, $db_pass_this); if($user_token == 'auth_firebase_error'){ echo 'Ошибка авторизации в базе данных.'; exit; } $url = 'https://vpnsergdudkotk.firebaseio.com/'.$user_token[1].'.json?auth='.$user_token[0]; |
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, «GET»);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
‘Content-Type: application/json’,)
);
$result = curl_exec($ch);
try {
$result_arr = json_decode($result, true);
$variable_this = $result_arr[‘adminmail’];
} catch(Exception $e) {
}
}
Функция получения токена авторизации — промежуточная и используется в двух последних. Те же, в свою очередь, возвращают крайнюю переменную результат работы в том же виде, в котором его возвращал MySQL.
Поменял запросы к базе данных в cert_send_toemail.php (настройки почты и сборка конфигурационного файла для пользователя) и build_server.php (сборка конфигурационного файла сервера), файл с настройками settings.php.
Собственно осталось отредактировать управляющий скрипт core_v1.php и core_web.php для возможности редактирования нового файла settings.php
Добавили строчку:
1 2 3 4 5 |
if(isset($command['db_key']) && (substr($str[$i], 0, 7) == '$db_key')){ $str[$i] = '$db_key = \''.$command['db_key'].'\'; //токен БД' . PHP_EOL; $flag = 1; } |
Скорректрировали «справку»:
1 2 |
echo 'Параметры: db_key:ключ API проекта(firebase) db_user:user(пользователь БД) db_pass:password(пароль пользователя БД) encoding:Europe/Minsk(таймзона)' . PHP_EOL; |
И удалили две лишние строки формирования settings.php: db_name, db_host
Для core-web.php все чуть сложнее (т.к. она у нас работает через json), итоговый код:
1 2 3 4 5 |
<!--?php <br ?--> $content = file_get_contents("php://input"); $command = json_decode($content, TRUE); header("Content-type: text/txt; charset=utf-8"); |
if(isset($command[‘build’])){
$exec = ‘php /etc/openvpn/scripts/build_’.$command[‘build’].’.php —args ‘;
if(!isset($command[‘server’]) || !isset($command[‘com’])){
helper();
}
if($command[‘build’] == ‘server’){
$exec .= ‘-‘.$command[‘com’] . ‘ ‘ . $command[‘server’];
}
if($command[‘build’] == ‘client’){
if(!isset($command[‘client’])){
helper();
}
if($command[‘com’] != ‘remove’){
if(!isset($command[’email’])){
helper();
}
}
$exec .= ‘-‘.$command[‘com’] . ‘ ‘ . $command[‘server’] . ‘ ‘ . $command[‘client’];
if(isset($command[’email’])){
$exec .= ‘ ‘ . $command[’email’];
}
}
}
if(isset($command[‘send’])){
if($command[‘send’] == ‘toemail’){
if(!isset($command[‘server’]) || !isset($command[’email’]) || !isset($command[‘client’])){
helper();
}
$exec = ‘php /etc/openvpn/scripts/cert_send_’.$command[‘send’].’.php —args ‘.$command[‘server’].’ ‘.$command[‘client’].’ ‘.$command[’email’].»;
}
}
if(isset($exec)){
exec($exec, $callback);
for($i=0;$i<count($callback);$i++){
echo $callback[$i];
if(($i+1)<count($callback)){
echo PHP_EOL;
}
}
echo PHP_EOL;
} else {
if(isset($command[‘settings’])){
settings($command);
} else {
helper();
}
}
function helper(){
echo ‘Доступные аргументы:’ .PHP_EOL;
echo ‘Вызов справки: -help’ .PHP_EOL;
echo ‘Создать/удалить openvpn сервер: build:server com:add(или remove) server:1(численный номер сервера)’ .PHP_EOL;
echo ‘Создать/удалить сертификаты клиента: build:client com:add(или remove) server:1(численный номер сервера, к которому будет привязан клиент) email:admin@sergdudko.tk client:1(численный номер клиентского сертификата)’ .PHP_EOL;
echo ‘Отправить сертификаты клиента на email: send:toemail(отправка на email) server:1(численный номер сервера, к которому будет привязан клиент) email:admin@sergdudko.tk client:1(численный номер клиентского сертификата)’ .PHP_EOL;
echo ‘Для настройки программы, наберите settings: {db_host:hostname(адрес БД MySQL или ip) db_name:name(имя БД) db_user:user(пользователь БД) db_pass:password(пароль пользователя БД) encoding:Europe/Minsk(таймзона)}’ .PHP_EOL;
echo PHP_EOL;
exit;
}
function settings($command){
$settings_file = «/etc/openvpn/scripts/settings.php»;
if(!file_exists($settings_file)){
echo ‘Файл с настройками не найден!’ . PHP_EOL;
exit;
}
$handle = @fopen($settings_file, «r»);
if ($handle) {
$i=0;
while (($buffer = fgets($handle, 4096)) !== false) {
$str[$i] = $buffer;
$i++;
}
if (!feof($handle)) {
echo «Error: unexpected fgets() fail\n»;
}
fclose($handle);
}
$current = »;
$flag = 0;
for($i=0;$i<count($str);$i++){ if(isset($command[‘settings’][‘db_key’]) && (substr($str[$i], 0, 7) == ‘$db_key’)){ $str[$i] = ‘$db_key = \».$command[‘settings’][‘db_key’].’\’; //токен БД’ . PHP_EOL; $flag = 1; } if(isset($command[‘settings’][‘db_user’]) && (substr($str[$i], 0, 8) == ‘$db_user’)){ $str[$i] = ‘$db_user = \».$command[‘settings’][‘db_user’].’\’; //пользователь БД’ . PHP_EOL; $flag = 1; } if(isset($command[‘settings’][‘db_pass’]) && (substr($str[$i], 0, 8) == ‘$db_pass’)){ $str[$i] = ‘$db_pass = \».$command[‘settings’][‘db_pass’].’\’; //пароль’ . PHP_EOL; $flag = 1; } if(isset($command[‘settings’][‘timezone’]) && (substr($str[$i], 0, 25) == ‘date_default_timezone_set’)){ $str[$i] = ‘date_default_timezone_set(\».$command[‘settings’][‘timezone’].’\’);’ . PHP_EOL; $flag = 1; } $current .= $str[$i]; } if($flag == 1) { if(file_put_contents($settings_file, $current)){ echo ‘Настройки изменены!’ . PHP_EOL; } } else { echo ‘Параметры: db_key:ключ API проекта(firebase) db_user:user(пользователь БД) db_pass:password(пароль пользователя БД) encoding:Europe/Minsk(таймзона)’ . PHP_EOL; echo ‘settings: обязательный префикс, лишние параметры можно пропустить’ . PHP_EOL; } exit; } exit; ?>
И соответственно добавили запрос вида:
1 2 3 4 5 6 7 8 |
{ "settings":{ "db_key":"token", "db_user":"user", "db_pass":"password", "encoding":"Europe/Minsk" } } |
Собственно серверная часть готова. Хотя нет, еще забыл вынести переменную имени проета firebase. Соответственно добавил в core_v1.php:
1 2 3 4 5 |
if(isset($command['db_projectname']) && (substr($str[$i], 0, 15) == '$db_projectname')){ $str[$i] = '$db_projectname = \''.$command['db_projectname'].'\'; //имя проекта firebase' . PHP_EOL; $flag = 1; } |
В веб-интерфейс core_web.php:
1 2 3 4 5 |
if(isset($command['settings']['db_projectname']) && (substr($str[$i], 0, 15) == '$db_projectname')){ $str[$i] = '$db_projectname = \''.$command['settings']['db_projectname'].'\'; //название проекта firebase' . PHP_EOL; $flag = 1; } |
В сам файл настроек строчку:
1 2 |
$db_projectname = '******'; //имя проекта firebase |
И в скриптах cert_send_toemail.php и build_server.php добавил в url переменную $db_projectname
Ну и запрос стал вида:
1 2 3 4 5 6 7 8 9 |
{ "settings":{ "db_key":"token", "db_projectname":"projectname", "db_user":"user", "db_pass":"password", "encoding":"Europe/Minsk" } } |
Теперь побеспокоимся о безопасности и добавим авторизацию. Чтобы не плодить логины и пароли — запросим те же данные (пользователь и пароль), что есть в файле настроек settings.php. Соответственно в core_web.php добавим следующую проверку:
1 2 3 4 5 6 7 |
/* проверка имени пользователя и пароля */ include(__DIR__ . '/settings.php'); if(($command['auth']['username'] != $db_user) || ($command['auth']['password'] != $db_pass)){ echo 'Авторизация не пройдена!' . PHP_EOL; exit; } |
И в каждый запрос нам нужно будет добавлять секцию с
1 2 3 4 5 |
"auth":{ "username":"user", "password":"password" } |
Т.е. пример изменения настроек:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "settings":{ "db_key":"token", "db_projectname":"projectname", "db_user":"user", "db_pass":"password", "encoding":"Europe/Minsk" }, "auth":{ "username":"user", "password":"password" } } |
Или создания клиентских сертификатов:
1 2 3 4 5 6 7 8 9 10 11 |
{ "build":"client", "com":"add", "server":"0", "email":"admin@sergdudko.tk", "client":"1", "auth":{ "username":"user", "password":"password" } } |
Соберем релиз, для чего во-первых нужно изменить наименование скрипта для проброса на внутренний сервер (чтобы оно не совпадало), изменим его на core-web-ctrl.php.
Скопируем сам скрипт в папку с проектом, туда же скопируем файл сервиса (unit). Для удобства решил изменить наименование проекта с dudko-web-panel на dwpanel.
Создадим папку dwpanel-0.1.0, в которую положим все наши файлы:
SendMailSmtpClass.php
build_client.php
build_server.php
cert_send_toemail.php
core_v1.php
settings.php
man.md
core-web-ctrl.php
core_web.php
dwpanel-server.service
Перепишем наш SPEC-файл для сборки, согласно правилам установки и удаления (ну и изменившегося наименования проекта):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Summary: OpenVPN WEB Panel Name: dwpanel Version: 0.1.0 Release: 1 License: GPL URL: https://vpn.sergdudko.tk Group: System/Configuration/Networking Source0: %{name}-%{version}.tar.gz BuildArch: noarch BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) Requires: php Requires: openvpn Requires: easy-rsa |
%description
This is Web panel for OpenVPN by Name Surname.
%prep
%setup -q
%build
%install
rm -rf %{buildroot}
install -d %{buildroot}%{_datadir}/%{name}
install SendMailSmtpClass.php %{buildroot}%{_datadir}/%{name}
install build_client.php %{buildroot}%{_datadir}/%{name}
install build_server.php %{buildroot}%{_datadir}/%{name}
install cert_send_toemail.php %{buildroot}%{_datadir}/%{name}
install core_v1.php %{buildroot}%{_datadir}/%{name}
install settings.php %{buildroot}%{_datadir}/%{name}
install man.md %{buildroot}%{_datadir}/%{name}
install core-web-ctrl.php %{buildroot}%{_datadir}/%{name}
install core_web.php %{buildroot}%{_datadir}/%{name}
install dwpanel-server.service %{buildroot}%{_datadir}/%{name}
%post
if ! [ -d /etc/openvpn/ ]; then
mkdir -p /etc/openvpn/
fi
if ! [ -d /etc/openvpn/scripts/ ]; then
mkdir -p /etc/openvpn/scripts/
fi
cp %{_datadir}/%{name}/SendMailSmtpClass.php /etc/openvpn/scripts/SendMailSmtpClass.php
cp %{_datadir}/%{name}/build_client.php /etc/openvpn/scripts/build_client.php
cp %{_datadir}/%{name}/build_server.php /etc/openvpn/scripts/build_server.php
cp %{_datadir}/%{name}/cert_send_toemail.php /etc/openvpn/scripts/cert_send_toemail.php
cp %{_datadir}/%{name}/core_v1.php /usr/bin/dwpanel
cp %{_datadir}/%{name}/settings.php /etc/openvpn/scripts/settings.php
cp %{_datadir}/%{name}/core_web.php /etc/openvpn/scripts/core_web.php
cp %{_datadir}/%{name}/dwpanel-server.service /etc/systemd/system/dwpanel-server.service
rm -rf %{buildroot}%{_datadir}/%{name}/
systemctl start dwpanel-server
systemctl enable dwpanel-server
echo Успешно установлено!
echo Пожалуйста, прочтите спецификацию /usr/share/dwpanel/man.md
%postun
systemctl disable dwpanel-server
systemctl stop dwpanel-server
rm -rf /etc/systemd/system/dwpanel-server.service
rm -rf /etc/openvpn/scripts/
rm -rf /usr/bin/dwpanel
echo Успешно удалено!
%files
%dir %{_datadir}/%{name}
%defattr(0755,root,root)
%{_datadir}/%{name}/SendMailSmtpClass.php
%{_datadir}/%{name}/build_client.php
%{_datadir}/%{name}/build_server.php
%{_datadir}/%{name}/cert_send_toemail.php
%{_datadir}/%{name}/core_v1.php
%{_datadir}/%{name}/settings.php
%{_datadir}/%{name}/core-web-ctrl.php
%{_datadir}/%{name}/core_web.php
%{_datadir}/%{name}/dwpanel-server.service
%defattr(0644,root,root)
%doc %{_datadir}/%{name}/man.md
%clean
rm -rf %{buildroot}
%changelog
* Tue Feb 01 2018 Name Surname
— Initial build
Соберем пакет:
1 2 3 |
rpmbuild -ba dwpanel.spec rpm -K ../RPMS/noarch/dwpanel-0.1.0-1.noarch.rpm |
Пакет в GPG-ключом: dwpanel-0.1.0-1.noarch.rpm