Category Archives: php

Защищаемся от спама по добавлению ip в чёрный список

Иногда боты достают конкретно, можно банить их по ip.

Для этого имеем в виду то, что ip в таблице со счётчиком num==0 нормален, а всё что выше – нет, и мы его откидываем.

Итак, строим такую таблицу:

CREATE TABLE `ips_blocked` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `dtm` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `ip` varchar(44) NOT NULL COMMENT 'ip адрес',
  `ua` tinytext NOT NULL COMMENT 'User agent',
  `num` int(11) NOT NULL DEFAULT '0' COMMENT 'кол-во попаданий',
  `rem` tinytext NOT NULL COMMENT 'примечание',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

ip туда добавляем вручную, т.к. этот процесс редкий, если нет – пишите скрипт для добавления туда ip.

По умолчанию num равен 0.

Итак, перед записью заявки проверяем ip по этой таблице, и если он есть и его номер num больше 0, откидываем заявку:

$ip = empty($_SERVER['REMOTE_ADDR']) ? '' : htmlspecialchars($_SERVER['REMOTE_ADDR']);
 
if(DB::columnIntPrepared('SELECT 1 FROM ips_blocked WHERE ip=? AND num>0', array($ip))) {
	DB::execute("UPDATE ips_blocked SET num=num+1 WHERE ip=?", array($ip));
	report('Ваш ip находится в чёрном списке. Пришлите вашу заявку нам на email.');
}

Таким образом мы можем пропустить первое случайное попадание в чёрный список по ip, а если оно повторится – всё, заявки с данного ip больше не принимаем совсем.

Таким образом, если мы опять обнаружили спамерский ip, ставим его номер 1 в таблице ips_blocked.

На этом всё, до свидания!

P.S. Для ip версии 4 (IPv4) наиболее эффективно наверное использовать тип INT(11),
и при этом каждый раз при добавлении/выборке его в/из таблицы надо использовать соотв. функции, например так:

Добавление: INSERT INTO ips_blocked VALUES (…, INET_ATON(?), …)
Выборка: SELECT INET_NTOA(ip) AS ip

Здесь вместо знака вопроса при добавлении вставляем ip в подготовленное SQL-выражение.

Но я не стал этого делать, а сделал тип поля VARCHAR и длину поля вообще 44, чтобы при случае иметь возможность добавлять IPv6, не знаю уже, когда этот момент настанет. ))))

Собственная капча на PHP своими руками за 5 минут

Приветствую вас!

Итак, мы наконец-то решились защитить свои формы капчей, т.к. спамеры вконец достали тупым спамом.

Хотя я и не приветствую капчи, т.к. они вынуждают совершить лишние телодвижения, да и могут оттолкнуть потенциальных клиентов от заполнения наших форм, но в ряде случаев они бывают жизненно необходимы.

Итак, в этом посте мы напишем собственный гибкий капча-механизм на PHP, который мы сможем поставить на свои формы за 5-10 минут.

Наша капча будет состоять из заданных латинских букв и арабских цифр, может иметь нужную нам длину и дополнительные артефакты в виде наклонов и линий, затрудняющих сканирование капчи ботами. Все эти параметры легко задаются в серверном скрипте, который будет формировать и показывать капчу.

Капчу мы будем рисовать на движке PHP GD, и для этого нам потребуются шрифт(ы) – да, можно использовать несколько шрифтов для некого затруднения работы по обходу капч.

Создадим папку fonts и закинем туда парочку бесплатных TTF-шрифтов, например, liber-mono.ttf и liber-sans.ttf. Сразу замечу, что полный комплект файлов – скрипты и шрифты вы можете скачать по ссылке: https://beotiger.com/download/jcaptcha.

Напишем наш серверный скрипт, который будет создавать и отображать капчу, назовём его скажем, jcaptcha.php

Вот этот скрипт с подробными комментариями:

// зададим имя куки для сохранения в ней кода капчи
define('CAPTCHA_COOKIE', 'imgcaptcha_');
 
/*
 
	Инициализируем генератор случайных чисел.
	Хотя в руководстве по PHP написано, что это делается автоматически
	каждый раз при запуске сценария, но... я им не верю 0_0
 
*/
 
mt_srand(time());
 
/*
 
	Определим путь к папке со шрифтами
	и список имен файлов со шрифтами в ней -
	из этого списка каждый раз будем выбирать случайный шрифт
 
*/
 
define('PATH_TTF', 'fonts/');
$fonts = array('liber-mono.ttf', 'liber-sans.ttf');
 
/*
 
	Основные параметры капчи.
 
	Для поддержки разных параметров капчи здесь можно	создать
	многомерный массив и обращаться к нему по индексу.
 
*/
 
$par = array(
	// ширина капчи
	'WIDTH' => 120,
	// высота капчи
	'HEIGHT' => 32,
	// размер шрифта на капче
	'FONT_SIZE' => 14,
 
	// кол-во символов на капче
	'CHARS_COUNT' => 5,
	// разрешенные символы капчи
	'ALLOWED_CHARS' => 'ABCDEFGHJKLMNPQRSTUVWXYZ23458',
 
	// фоновый цвет капчи - белый в нашем случае
	'BG_COLOR' => '#FFFFFF',
	// кол-во линий на капче
	'LINES_COUNT' => 3,
	// толщина линий
	'LINES_THICKNESS' => 2
);
 
/*
	Общие парметры капчи
*/
 
// цвета символов
define('CODE_CHAR_COLORS', '#880000,#008800,#000088,#888800,#880088,#008888,#000000');
// цвета линий
define('CODE_LINE_COLORS', '#880000,#008800,#000088,#888800,#880088,#008888,#000000');
 
// получаем цвета линий и символов в массивы для случайной выборки позднее
$line_colors = preg_split('/,\s*?/', CODE_LINE_COLORS);
$char_colors = preg_split('/,\s*?/', CODE_CHAR_COLORS);
 
// создаем пустой рисунок и заполняем его белым фоном
$img = imagecreatetruecolor($par['WIDTH'], $par['HEIGHT']);
imagefilledrectangle($img, 0, 0, $par['WIDTH'] - 1, $par['HEIGHT'] - 1, gd_color($par['BG_COLOR']));
 
// устанавливаем толщину линий и выводим их на капчу
imagesetthickness($img, $par['LINES_THICKNESS']);
 
for ($i = 0; $i < $par['LINES_COUNT']; $i++)
    imageline($img,
        mt_rand(0, $par['WIDTH'] - 1),
        mt_rand(0, $par['HEIGHT'] - 1),
        mt_rand(0, $par['WIDTH'] - 1),
        mt_rand(0, $par['HEIGHT'] - 1),
        gd_color($line_colors[mt_rand(0, count($line_colors) - 1)])
    );
 
// Переменная для хранения кода капчи
$code = '';
 
// Зададим координату по центру оси Y 
$y = ($par['HEIGHT'] / 2) + ($par['FONT_SIZE'] / 2);
 
// Выводим символы на капче
for ($i = 0; $i < $par['CHARS_COUNT']; $i++) {
		// выбираем случайный цвет из доступного набора
    $color = gd_color($char_colors[mt_rand(0, count($char_colors) - 1)]);
    // определяем случайный угол наклона символа от -45 до 45 градусов
    $angle = mt_rand(-45, 45);
    // выбираем случайный символ из доступного набора
    $char = substr($par['ALLOWED_CHARS'], mt_rand(0, strlen($par['ALLOWED_CHARS']) - 1), 1);
    // выбираем случайный шрифт из доступного набора
    $font = PATH_TTF . $fonts[mt_rand(0, count($fonts) - 1)];
    // вычислим координату текущего символа по оси X
    $x = (intval(($par['WIDTH'] / $par['CHARS_COUNT']) * $i) + ($par['FONT_SIZE'] / 2));
 
    // выводим символ на капчу
    imagettftext($img, $par['FONT_SIZE'], $angle, $x, $y, $color, $font, $char);
 
    // сохраняем код капчи
    $code .= $char;
}
 
// сохраним капчу в куках для дальнейшей проверки
setcookie(CAPTCHA_COOKIE, md5($code));
 
/*
 
	Посылаем сформированный рисунок в браузер и избавляемся от него, 
	хотя сборщик мусора обычно это делает за нас
 
*/
 
header("Content-Type: image/png");
imagepng($img);
imagedestroy($img);
 
// Преобразуем HTML 6-символьный цвет в GD цвет 
function gd_color($html_color)
{
  return preg_match('/^#?([\dA-F]{6})$/i', $html_color, $rgb)
    ? hexdec($rgb[1]) : false;
}

Вот как выглядит сформированная данным скриптом капча (щёлкните на ней для смены кода):

Капча

Теперь для создании формы с капчей достаточно добавить в неё дополнительный инпут с произвольным именем и разместить капчу, например так:

<form action="go.php" method="post">
	Введите имя: <input name="name"><br>
	Введите email: <input name="email"><br>
	Введите код с картинки: <input name="captcha">
	<img title="Щёлкните для нового кода" alt="Капча" src="jcaptcha.php" style="border: 1px solid black" onclick="this.src='jcaptcha.php?id=' + (+new Date());"><br>
	<input type="submit" value="Отправить!">
</form>

А в скрипте go.php после получения данных с формы, но перед дальнейшей их обработкой нужно будет проверить код капчи, и если он не совпадает с заданным, вывести соответствующее сообщение и вернуться к форме, например, так:

 
	// зададим имя куки для получения из неё кода капчи,
	// оно конечно же должно совпадать с соотв. именем в jcaptcha.php
	define('CAPTCHA_COOKIE', 'imgcaptcha_');
	// заметим: поле `captcha` обязательно для заполнения
	if(empty($_POST['captcha']) || md5($_POST['captcha']) != @$_COOKIE[CAPTCHA_COOKIE])
		die('Неверный код с картинки. Вернитесь и повторите попытку.');

Приведенный здесь код дан лишь для примера, в реальных условиях проверять капчу и выводить соотв. сообщение лучше ч/з AJAX, не покидая форму и не заставляя клиента каждый раз вводить одни те же данные по нескольку раз, с возможным таймаутом при превышении числа неудачных попыток для предотвращения брутфорс-атаки.

В заключении замечу, что не стоить создавать слишком сложных, плохо читаемых капч, т.к. это может отпугнуть клиентов от вашего сайта, а лучше в идеале совсем обходиться без капчи. Также стоит давать возможность сменить код капчи без перезагрузки страницы, как и продемонстрировано в нашем примере с формой, где обработчик onclick капчи обновляет её каждый раз, когда на ней щёлкают.

Итак, вы можете скачать полный комплект файлов данного примера – скрипты и шрифты по ссылке: https://beotiger.com/download/jcaptcha

А теперь – пока, пока.
До свидания, до новых встреч, друзья!

GifCreator: создаём динамические GIF-ки своими руками на PHP

Введение

Здравствуйте.

Встала задача – показывать на сайте GIF-ку с текстом, который со временем должен меняться. В случае статического рисунка проблем нет, смотрите хотя бы наш пост PHP-торт на день Рождения!, в котором рассказано как писать текст, в том числе Кириллицей, на картинке с возможностью поворота и центрирования его по нужным осям.

Но на GIF’ке с несколькими кадрами как мы текст напишем? Надо доставить каждый кадр, обновлять на нём текст и собирать кадры в GIF-ку снова.

Вот к примеру дана нам такая GIF-ка всего с 2-мя кадрами, закольцованными, меняющимися примерно каждые 2-3 секунды:

openprice.gif

Внизу вы видите текст:

До окончания акции: 3 дня
Осталось: 5 курсов

Сразу понятно, что тут надо ежедневно менять кол-во оставшихся дней. А кол-во оставшихся курсов может поменяться в любой момент времени, и тогда нам надо будет обновить картинку соответственно.

Что же делать?

Как это сделать, спросите вы? GifCreator.php нам в руку, отвечу я. Проект на GitHub’е здесь: https://github.com/Sybio/GifCreator

С помощью этой небольшой либы можно создавать GIF-ки на PHP своими руками.

Пройдемся по циклу создания GIF-ки, а потом напишем собственный скрипт, который будет автоматически обновлять нашу GIF-ку кол-вом оставшихся до окончания акции дней и кол-вом оставшихся курсов.

Основы создания новой GIF-ки

Итак, процесс создания GIF-ки с помощью GifCreator таков:

Определяем массив, содержащий в себе пути к файлам рисунков, GD Image-ресурсам (созданным с помощью таких функций как imagecreatefromjpeg и т.п.), а также сырыми (raw) данными содержимого рисунков.
К примеру:

$frames = array(
    imagecreatefrompng('images/pic1.png'), // ресурс GD Image 
    'images/pic2.png', // путь к рисунку на диске 
    file_get_contents('images/pic3.jpg'), // сырое содержимое рисунка  
    'https://mydom.ru/images/pic4.jpg', // URL-путь к рисунку в сети
);

Далее определяем массив с продолжительностью кадров в миллисекундах.

$durations = array(40, 80, 40, 20);

Далее создаём саму GIF-ку

$gc = new GifCreator\GifCreator();
$gc->create($frames, $durations, 5);

Третий параметр метода .create() определяет количество повторов кадров GIF-ки. В данном случае мы повторяем кадры GIF-ки 5 раз. Можно задать 0 для бесконечного повтора.

И наконец получаем результат:

$gif = $gc->getGif();

Теперь мы можем сохранить нашу GIF-ку на диске:

file_put_contents('images/pic.gif', $gif);

или вывести прямиком в браузер:

header('Content-type: image/gif');
die($gif);

Важные особенности

  • Прозрачность основывается на первом фрэйме и сохранится только в том случае, если мы задаём фрэймы с одинаковой прозрачностью.
  • Размеры генерируемой GIF-ки также базируются на первом фрэйме, то есть все остальные фреймы будут подогнаны по размеру к нему.

Практика

Итак, мы делаем естественный шаг от теории к практике. Возьмем нашу заданную вначале поста GIF-ку и напишем на ней актуальный на данный момент текст. Поехали по шагам.

Первое, разбираем GIF-ку на фреймы. Как это сделать, спросите вы. Без понятия, отвечу я. Гоголь или Яндекс вам в помощь.

Получаем наши заветные фрэймы:

Frame0.gif 
Frame0.gif  
Frame1.gif
Frame1.gif

В любом тектсовом, ой, графическом редакторе очищаем нужные области от текста, и сохраняем результат в файлики Frame0.png и Frame1.png.

Результат может выглядеть так:

Frame0.png   Frame1.png

Выкладываем файлики в заданное место на сервер и пишем следующий скрипт,
в комментариях к которому многое поясняется, а пару моментов я дам ниже:

// Подключаем класс создания GIF-ок
include('GifCreator.php');
 
// Определим сколько дней осталось до окончания акции
 
// сейчас
$datetime1 = new DateTime();
 
// до какого числа: выберем случайный промежуток 0 - 7 дней
$datetime2 = new DateTime(date('Y-m-d', time() + rand(80000, 604800)));
 
$interval = $datetime1->diff($datetime2);
$days_left = $interval->format('%d') + 1;
 
// определим окончание для слова ДЕНЬ (дня/дней/день)	
$ln = substr($days_left, -1, 1); // последняя цифра числа
if($days_left < 21 && $days_left > 4)
	$days = 'дней';
elseif($ln >= 2 && $ln < 5)
	$days = 'дня';
elseif($ln == 1)
	$days = 'день';
else
	$days = 'дней';
 
// сколько курсов осталось
mt_srand(time());
$num = toZero(15 - mt_rand(0, 15));
 
// готовим наши динамические тексты, функцию Suffix смотрите внизу
$text1 = "До окончания акции: $days_left $days";
$text2 = "Осталось: $num курс" . Suffix($num);
 
/*
 
	Для Кириллицы - используем TTF-шрифты,
	Times New Roman Bold для данного примера.
	Путь по умолчанию для шрифта - в текущем каталоге.
 
*/
 
putenv('GDFONTPATH=' . realpath('.')); 
$font = 'timesbd'; // название шрифта без расширения ttf
 
/*
 
 Размер символов ($size) и начальная координата по вертикали ($y),
	подбираются опытным путём
 
*/
 
$size = 11;
$y = 310;
 
// Открываем первый фрэйм и добавляем тексты по центру внизу друг под другом чёрным цветом
$frame0 = imagecreatefrompng('gif/Frame0.png');
$black = imagecolorallocate($frame0, 0, 0, 0);
 
$x1 = getX($size, 0, $text1, $font, $frame0);
$x2 = getX($size, 0, $text2, $font, $frame0);
imagettftext($frame0, $size, 0, $x1, $y, $black, $font, $text1);
imagettftext($frame0, $size, 0, $x2, $y + 20, $black, $font, $text2);
 
// Тоже самое проделываем для второго фрэйма, координата по X нам уже известна
// Помним, что фрэймы у нас должны быть всегда одного размера
$frame1 = imagecreatefrompng('gif/Frame1.png');
$black = imagecolorallocate($frame1, 0, 0, 0);
imagettftext($frame1, $size, 0, $x1, $y, $black, $font, $text1);
imagettftext($frame1, $size, 0, $x2, $y + 20, $black, $font, $text2);
 
 
/*
 
	Итак, добавляем созданные нами кадры в массив и определяем
	продолжительность задержки между кадрами в 2 секунды
 
*/
 
$frames = array($frame0, $frame1);
$durations = array(200, 200);
 
/*
	Создаём GIF-ку, анимация зациклена (3-ий параметр в create() - 0)
*/
 
$gc = new GifCreator\GifCreator();
$gc->create($frames, $durations, 0);
 
// Получаем GIF-ку и выводим её в браузер
$gif = $gc->getGif();
 
header('Content-type: image/gif');
die($gif);
 
/* ********************************
 
	Вспомогательные функции
 
******************************** */
 
/*
 
	getX($size, $angle, $text, $font, $im)
	Центруем текст по оси X (горизонтали) на рисунке
 
	Вход:
		@size - размер символов текста
		@angle - угол поворота текста
		@text - сам текст
		@font - название шрифта
		@im - ресурс GD Image
 
	Выход: нужная координата по оси X
 
*/
 
function getX($size, $angle, $text, $font, $im)
{
	$ts = imagettfbbox($size, $angle, $font, $text);
	$dx = abs($ts[2] - $ts[0]);
	return (imagesx($im) - $dx) / 2;
}
 
/*
 
	Возвращает 0, если аргумент меньше нуля, иначе - сам аргумент
 
*/
 
function toZero($val)
{
  return ($val > 0 ? $val : 0);
}
 
/*
 
	Вернуть правильное окончание слова `курс`
	в зависимости	от переданного числа
 
	Правильно для числа от 0 до 110 (> 100 надо дорабатывать)
 
	@returns {String} окончание
 
*/
 
function Suffix($num)
{
	if($num > 10 && $num < 20)
		return 'ов';
 
	// смотрим последнюю цифру
  $n = intval(substr(strval($num), -1, 1));
 
 	return $n == 1 ? '' : ($n < 5 && $n > 0 ? 'а' : 'ов');
}

Файл GifCreator.php с классом GifCreator можно скачать с GitHub’а – ссылку я давал выше, либо взять с архива с примерами – ссылку я даю ниже. 😉

Также при создании картинок с текстом необходимо подключать нужные шрифты, в данном примере мы подключили шрифт Times New Roman Bold из файла timesbd.ttf. Путь к файлу шрифта надо задавать либо напрямую в имени шрифта, либо через переменную окружения GDFONTPATH, как и показано в нашем примере. В Windows тут надо проявлять большую осмотрительность и осторожность.

В данном примере меняющиеся данные для текстов мы создаём рандомно, в реальных проектах они конечно же вычисляются на основе реальных вещей.

Результат

Вот результат работы нашего скрипта.

GifCreate

Заметьте, что при каждом обновлении странички надпись будет меняться.

Также при нажатии на картинку она обновится и покажет новые надписи, но не злоупотребляйте этим, всё таки хоть и малая, а нагрузка на сервак какая-никакая создаётся, так что стоит наверное рассмотреть возможность кэширования таких динамически создаваемых GIF-ок, особенно для страничек с большими нагрузками.

Скачивайте полный рабочий архив со всеми файликами ресурсов и скриптами отсюда:
https://beotiger.com/download/gifcreate, заваривайте крепкий чай с вишнёвым вареньем и наслаждайтесь жизнью и классной погодой за окном.

А мне остаётся только раскланяться с вами, поздравить с наступающими праздниками и началом чемпионата мира по футболу.

Вуаля, адью, гудбай, ауфидерзеен, до свиданья, чаю, пока.

Настройка сервера с нуля на примере VPS от Hetzner’а

Hetzner.de – выбираем сервер

Итак, закажем и настроим свой собственный VPS. Заходим на https://www.hetzner.de/, в верхнем меню выбираем VServer (виртуальный сервер). Давайте выберем средний по цене, но вроде как с неплохими характеристиками: vServer CX30

Обратим внимание на цену: на немецком он предлагается за 14,16€ / Monat (14,16 евро в месяц), а после переключения на русский язык или USA: € 11.90 per month / 11,90€ / месяц.

Что за? Да, немцы своим дороже что ли продают? У них денег, видимо больше. Да ладно, переведём евро на рубли, сейчас Яндекс показывает 63,57 руб./ 1EUR – курс ЦБ РФ на 28/12/2016. Округлим до 65, получается сумма менее 1.000 руб: 11,90€ * 65 руб. = 773.50 руб/месяц.

Я думаю, неплохо, особенно после того как посмотрим на предлагаемую конфигурцию сего чуда:
Benefits: 2 vCores, RAM 4 GB RAM, SSD 100 GB, Connection: 1 Gbit/s NIC, Traffic: 8 TB, Snapshots: 3. Что в переводе на русский означает 2 ядра (виртуальных), 4 гига оперативки, диск на 100 гигов (SSD), скорость связи 1 Гигабит/секунда, предел трафика – 8 Терабайт. Сразу замечу, что в комментарии указано, что при превышении предела трафика за месяц скорость подключения снизится до 10 Мегабит в секунду, но сервер по прежнему будет доступен. Да, и ещё в комплектацию входит поддержка до 3 снэпшотов, так что при желании можно откатиться.

Заказываем сервер

Ну что, друзья, неплохо за такие деньги или как? ОК, поехали! Вперед, без сомнений, этот сервер будет наш. Нажимаем кнопочку “Order now” (“Заказать” – да, сайт Hetzner.de поддерживает русский в том числе, да вообще много языков, но вот насчёт самой тех.поддержки не уверен, думаю, английским обойдёмся если что). А сама админка сервера (https://robot.your-server.de/server) у меня на английском, я честно говоря, доволен и так.

Далее нам предстоит выбрать систему из списка:

Operating systems without pre-installed control panel

  • CentOS 6.8 minimal
  • CentOS 7.2 minimal
  • Debian 8.6 LAMP
  • Debian 8.6 minimal
  • openSUSE 42.1 minimal
  • Ubuntu 16.04.1 LTS minimal
  • Ubuntu 16.10 minimal
  • Windows Server 2012 R2 Datacenter Edition (Price (monthly): € 130.25 / Setup (once): € 0.00)
  • Windows Server 2012 R2 Standard Edition (Price (monthly): € 21.01 / Setup (once): € 0.00)

Operating systems with pre-installed control panel

  • CentOS 7.2 + cPanel
  • CentOS 7.2 + Plesk
  • Debian 8.6 + Plesk
  • Ubuntu 16.04.1 LTS + Plesk
  • Windows Server 2012 R2 Datacenter Edition + Plesk (Price (monthly): € 130.25 / Setup (once): € 0.00)
  • Windows Server 2012 R2 Standard Edition + Plesk (Price (monthly): € 21.01 / Setup (once): € 0.00)

То есть два списка – без контроль-панели и с ней (cPanel/Plesk).

Ребята, мы выбираем – Ubuntu 16.10 minimal без всяких панелей. Это минимальная система, в ней как я понял, дополнительно будут установлены основные системные утилиты (coreutils) и SSH. Как раз то, что нам нужно. Всё остальное – NGINX/PHP/MYSQL/POSTFIX/DOVECOT/PUREFTPD/SpamAssassin/FAIL2BAN и т.д. и т.п. (по-русски etc.) мы установим сами, своими собственными ручками, тем более это не так уж и сложно и не так уж и долго, в частности благодаря APT‘у.

На нашем сервере будет работать 3-4 сайта на NGINX/PHP-FPM, крутится радио и, возможно, экспериментальный сайтик на node.js. Также мы поставим FFMPEG и ImageMagic для беспрепятственной обработки видео/аудио/графики.

Итак, далее идёт опция – выбрать SSH-доступ ч/з пароль или через ключи, я выбрал пароль, но это дело вкуса, тем более потом всегда можно всё поменять.

Потом, если у Вас нет аккаунта на Hetzner.de, Вам предложат его создать, ничего особенного, доволньо небольшая форма, и опосля предложат ввести данные своей банковской карты. Так как я этому сайту доверяю (кто не знает Hetzner.de?) я без страха ввожу все нужные данные и… получаю сервер в свои руки.

Рабочая среда в Windows 10

Мне на почту пришло письмо буквально через 5-10 минут, что сервер готов к работе, здесь же мне указали рутовый пароль и ip-адрес нашего сервера. Итак, открываем свой любимый SSH-клиент, например, Putty и поехали!

Да, кстати, в своей Windows 10 я использую связку WinSCP + Putty. То есть захожу на файловую систему сервера под рутом через WinSCP по SFTP протоколу, здесь же могу копировать/удалять/править в виндовом редакторе (мой любимый сейчас AkelPad) любой файл, а нажав на кнопочку Ctrl+P (Open session in PuTTY) я моментально, без ввода пароля, попадаю в консоль PuTTY (конечно, всё это надо настроить в WinSCP предварительно, что делается буквально в несколько кликов мышки).

Настройка DNS

Лично я использую бесплатный Hurricane Electric Free DNS Management – https://dns.he.net/
А Hetzner.de предлагает подобную вещь за:

Nameserver Robot

    Administer DNS entries

more information...
Price (once): € 15.97
For Dedicated Root Server and vServer customers free of charge

Это 15,97 евро * 65 рублей = 1038,05 рублей. Это разовая оплата. Чем он лучше бесплатного? На бесплатный нельзя полагаться на 100%, хотя мы работаем с Hurricane Electric несколько лет и ни разу не было проблем (в отличие от https://entrydns.net/, который часто глючил, а потом стал просить денег, хоть и не больших).

Но тут нам раздумывать нечего – видите надпись – Free of charge for dedicated root server and vServer customers? Да, для нашего виртульаного сервера он должен быть бесплатным. Но при заходе на https://robot.your-server.de/ мы нигде не видим ссылку на что-то подобное NS/DNS. Оказывается, его просто надо заказать дополнительно – заходим по ссылкам: Ordering – Domain Administration – Nameserver Robot – Order Product и видим:

Shopping cart
  	  	Unit price 	Total price 	 
  	  	Monthly 	Setup 	Monthly 	Setup 	 
1 x 	Nameserver Robot 	€ 0.00 	€ 0.00 	€ 0.00 	€ 0.00 	
Total: 	€ 0.00 	€ 0.00 	 

Да, везде нули, в данном случае это хорошо, он для нас действительно вроде как бесплатен.

Но, повторяюсь, я использую Hurricane Electric, поэтому здесь не стал заказывать.

Как настроить начальные и самые необходимые NS записи для сайтов рассказано здесь, на этом блоге (на примере Hurricane Electric Free DNS Management) – http://atzar.ru/hurricane-electric-free-dns-management/. На Hetzner’е, думаю, настравается всё подобным образом. Настраиваем всё под наш новый ip сервера.

Первые шаги

Итак, смотрим, что уже у нас в действительности есть, держа в уме, что всё же это виртуалка, а не полноценный сервер. Но у нас пока нет десятков тысяч – миллионов клиентов, чтобы держать полноценный сервер или кластер таких серверов.
Все команды я будут выполнять от рута, так как у меня нет паранойи по этому поводу, ты через sudo сможешь накосячить также как и не через sudo. Да ладно, это дело в куса а не принципа.
Нас встречает приглашние вида:
root@Ubuntu-1610-yakkety-64-minimal ~ #

FQDN

Заздадим Fully Qualified Domain Name:
# hostname beotiger
# echo beotiger > /etc/hostname
# vim /etc/hosts
ip-адрес нашего сервера site2.ru

Теперь перелогинимся и получим приглашение root@beotiger ~ #

Итак, версия:

# uname -a
Linux beotiger 4.8.0-32-generic #34-Ubuntu SMP Tue Dec 13 14:30:43 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

# cat /etc/issue
Ubuntu 16.10 \n \l

Сколько памяти свободно:

# free -h
total used free shared buff/cache available
Mem: 3.9G 218M 2.8G 17M 841M 3.4G
Swap: 0B 0B 0B

Дисковое пространство:

# df -h
Filesystem Size Used Avail Use% Mounted on
udev 2.0G 0 2.0G 0% /dev
tmpfs 396M 8.7M 387M 3% /run
/dev/sda1 94G 2.1G 87G 3% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup
tmpfs 396M 0 396M 0% /run/user/0

Итак, видим, что наша пока пустая система сжирает 218Мегабайт оперативки и 2.1Гиг нашего диска. Посмотрим, что будет после установки всех нужных нам сервисов! Для удобного просмотра состояние системы и процессов можно установить htop:

# apt install htop

Язык и timezone

Лично я предпочитаю английский в документации, сказывается давняя паранойя о плохом переводе. Поэтому локаль не меняю. А время всё же поменяю на московское, а то в логах буду путаться (я живу по московскому времени):

# apt install tzdata
# dpkg-reconfigure tzdata

Выбираем пояс Europe/Moscow, и ребутнемся, вспомним времена Win98 (чтобы все наши уже запущенные службы стали использовать новый часовой пояс в логах и т.п.):

# reboot

Синхронизация времени

Внимание! NTP не нужен, если есть timesyncd и мы не собираемся выступать в роли ntpd сервера. Пакет: systemd: /lib/systemd/systemd-timesyncd
# timedatectl status
Local time: Fri 2016-12-30 12:13:22 MSK
Universal time: Fri 2016-12-30 09:13:22 UTC
RTC time: Fri 2016-12-30 09:13:22
Time zone: Europe/Moscow (MSK, +0300)
Network time on: yes
NTP synchronized: yes
RTC in local TZ: no

Строка NTP synchronized: yes говорит о том, что время синхронизируется успешно.

Устанавливаем LEMP-стек: NGINX + PHP 7.0 + MySQL

LAMP: Linux Apache MySQL PHP – минимальная готовая среда веб-разработки. Linux у нас есть, Apache мы поменяем на Nginx. Т.е. LAMP -> LEMP (Linux Nginx MySQL PHP). Итак, ставим, настраивать будем позже:

NGINX

NGINX самая простая и мощная вещь, как раз то, что я люблю больше всего:

# apt install nginx

Всё, у нас есть рабочий сервер. Уже можно зайти на http://ip-нашего-сервера и узреть стандартную дефолтную страницу nginx. Дефолтная папка сайта /var/www/html. Мы её трогатиь не будем, когда поставим PHP/MySQL, займёмся детальной настройкой и связкой NGINX+PHP7 (напомню, будем добавлять несколько сайтов)

Текущая версия nginx:

# nginx -v
nginx version: nginx/1.10.1 (Ubuntu)

Посмотреть текущие модули nginx (где-то видел хак на stackoverflow):

# 2>&1 nginx -V | tr -- - '\n' | grep _module
http_ssl_module
http_stub_status_module
http_realip_module
http_auth_request_module
http_addition_module
http_dav_module
http_geoip_module
http_gunzip_module
http_gzip_static_module
http_image_filter_module
http_v2_module
http_sub_module
http_xslt_module
stream_ssl_module
mail_ssl_module

В стандартном nginx-пакете есть только самые необохдимые модули. Если Вам нужны допонлительные модули, например, MP4, совсем не обязательно перекомпилировать nginx с сорцов.
Добавить модули в nginx можно так (ещё не пробовал):

# apt install nginx-extras

Возможная конфигурация с использованием модуля mp4:

location /video/ {
mp4;
mp4_buffer_size 1m;
mp4_max_buffer_size 5m;
mp4_limit_rate on;
mp4_limit_rate_after 30s;
}

Смотрим, что содержиться в nginx-extras:

# apt show nginx-extras
Package: nginx-extras
Version: 1.10.1-0ubuntu1.2
Priority: optional
Section: universe/httpd
Source: nginx
Origin: Ubuntu
Maintainer: Ubuntu Developers
Original-Maintainer: Kartik Mistry
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 1,886 kB
Provides: httpd, httpd-cgi, nginx
Depends: nginx-common (= 1.10.1-0ubuntu1.2), perl (>= 5.22.2-3), perlapi-5.22.2, libc6 (>= 2.14), libexpat1 (>= 2.0.1), libgd3 (>= 2.1.0~alpha~), libgeoip1, libluajit-5.1-2, libpam0g (>= 0.99.7.1), libpcre3, libperl5.22 (>= 5.22.2), libssl1.0.0 (>= 1.0.2~beta3), libxml2 (>= 2.7.4), libxslt1.1 (>= 1.1.25), zlib1g (>= 1:1.1.4)
Suggests: nginx-doc (= 1.10.1-0ubuntu1.2)
Conflicts: nginx-core, nginx-full, nginx-light
Breaks: nginx (<< 1.4.5-1) Homepage: http://nginx.net Download-Size: 630 kB APT-Sources: http://mirror.hetzner.de/ubuntu/packages yakkety-updates/universe amd64 Packages Description: nginx web/proxy server (extended version) Nginx ("engine X") is a high-performance web and reverse proxy server created by Igor Sysoev. It can be used both as a standalone web server and as a proxy to reduce the load on back-end HTTP or mail servers. . This package provides a version of nginx with the standard modules, plus extra features and modules such as the Perl module, which allows the addition of Perl in configuration files. . STANDARD HTTP MODULES: Core, Access, Auth Basic, Auto Index, Browser, Empty GIF, FastCGI, Geo, Limit Connections, Limit Requests, Map, Memcached, Proxy, Referer, Rewrite, SCGI, Split Clients, UWSGI. . OPTIONAL HTTP MODULES: Addition, Auth Request, Charset, WebDAV, FLV, GeoIP, Gunzip, Gzip, Gzip Precompression, Headers, HTTP/2, Image Filter, Index, Log, MP4, Embedded Perl, Random Index, Real IP, Secure Link, SSI, SSL, Stream, Stub Status, Substitution, Thread Pool, Upstream, User ID, XSLT. . MAIL MODULES: Mail Core, Auth HTTP, Proxy, SSL, IMAP, POP3, SMTP. . THIRD PARTY MODULES: Auth PAM, Cache Purge, DAV Ext, Echo, Fancy Index, Headers More, Embedded Lua, HTTP Push, HTTP Substitutions, Upload Progress, Upstream Fair Queue.

Много всего, нужно ли оно нам сейчас?! Как подключать/отключать нужные модули при запуске сервера/службы?

PHP 7.0

Конечно, будем стаить семёрку. По заявлению многих, она намного быстрее пятёрки. Не знаю, не проверял, но охотно верю! Заметьте, что пакет PHP7.0, который описывается так:

php7.0/yakkety,yakkety,yakkety,yakkety 7.0.8-3ubuntu3 all
server-side, HTML-embedded scripting language (metapackage)

предложит нам поставить apache, я не знаю почему, что в головах творится у этих собирателей пакетов под Юбунту? Сами смотрите:

# apt install php7.0
The following additional packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils libapache2-mod-php7.0 libapr1
  libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap liblua5.1-0 php-common
  php7.0-cli php7.0-common php7.0-json php7.0-opcache php7.0-readline psmisc
  ssl-cert
 
The following NEW packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils libapache2-mod-php7.0 libapr1
  libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap liblua5.1-0 php-common php7.0
  php7.0-cli php7.0-common php7.0-json php7.0-opcache php7.0-readline psmisc
  ssl-cert
0 upgraded, 19 newly installed, 0 to remove and 0 not upgraded.
Need to get 5,066 kB of archives.
After this operation, 20.6 MB of additional disk space will be used.
Do you want to continue? [Y/n]n [Aborted]

Nooooooooo, тут нажимаем n, нет, nicht, nope, ни в коем случае. Вы что творите, сборщики пакетов для Убунты?

Пакет php7.0-fpm:

php7.0-fpm/yakkety,yakkety 7.0.8-3ubuntu3 amd64
server-side, HTML-embedded scripting language (FPM-CGI binary)

# apt install php7.0-fpm
	Reading package lists... Done
	Building dependency tree
	Reading state information... Done
	The following additional packages will be installed:
	  php-common php7.0-cli php7.0-common php7.0-json php7.0-opcache php7.0-readline
	  psmisc
	Suggested packages:
	  php-pear
	The following NEW packages will be installed:
	  php-common php7.0-cli php7.0-common php7.0-fpm php7.0-json php7.0-opcache
	  php7.0-readline psmisc
	0 upgraded, 8 newly installed, 0 to remove and 0 not upgraded.
	Need to get 3,570 kB of archives.
	After this operation, 14.3 MB of additional disk space will be used.
	Do you want to continue? [Y/n] Y
 
	Creating config file /etc/php/7.0/cli/php.ini with new version
	Setting up php7.0-fpm (7.0.8-3ubuntu3) ...
 
	Creating config file /etc/php/7.0/fpm/php.ini with new version
	Created symlink /etc/systemd/system/multi-user.target.wants/php7.0-fpm.service → /lib/systemd/system/php7.0-fpm.service.

ИЗ: https://php-fpm.org/
PHP-FPM (FastCGI Process Manager) альтернатива PHP FastCGI со множеством новых полезных возможностей, подходящих для сайтов любых размеров, особенно для загруженных сайтов. Включают: Адаптивный запуск процессов! Статистика! These features include: Adaptive process spawning (NEW!) Basic statistics (ala Apache's mod_status) (NEW!)

Итак, мы видим, что установились основные пакеты PHP7.0 - common, cli (для вызова PHP из командной строки), fpm - как раз то, что нам нужно для связи с NGINX, нашим вебсервером, json - полезная штука, мы её применяем практически во всех наших проектах, особенно основанных на AJAX, opcache - кэширование, readline - возможность проводить с PHP сессию типа интерактивного шелла, подробнее см. здесь: http://php.net/manual/en/features.commandline.interactive.php

Доустановим некоторые наиболее важные и нужные PHP-пакеты, часто используемые не только в наших проектах:

# apt install php7.0-curl php7.0-gd php7.0-mbstring php7.0-mcrypt php7.0-mysql php7.0-sqlite3
...
The following additional packages will be installed:
  libcurl3 libmcrypt4
Suggested packages:
  libmcrypt-dev mcrypt
The following NEW packages will be installed:
  libcurl3 libmcrypt4 php7.0-curl php7.0-gd php7.0-mbstring php7.0-mcrypt php7.0-mysql
  php7.0-sqlite3

curl - общение с сетью из PHP ч/з удобный CURL API, gd - рисование графических примитов и текста, mbstring - поддержка мультибайтовых строк, mcrypt - криптование, кому оно сейчас не треба)), mysql и sqlite3 - именно эти БД я использую в своих проектах, вам могут понадобиться другие БД, например pgsql или sybase. Вообще список доступных пакетов PHP 7 можно посмотреть например так:

# apt search php7
Sorting... Done
Full Text Search... Done
libapache2-mod-php7.0/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  server-side, HTML-embedded scripting language (Apache 2 module)

libphp7.0-embed/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  HTML-embedded scripting language (Embedded SAPI library)

php-all-dev/yakkety,yakkety,yakkety,yakkety 1:44 all
  package depending on all supported PHP development packages

php-symfony-polyfill-php70/yakkety,yakkety,yakkety,yakkety 1.2.0-1 all
  Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions

php7.0/yakkety-updates,yakkety-updates,yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 all
  server-side, HTML-embedded scripting language (metapackage)


php7.0-bcmath/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Bcmath module for PHP

php7.0-bz2/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  bzip2 module for PHP

php7.0-cgi/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  server-side, HTML-embedded scripting language (CGI binary)

php7.0-cli/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed,automatic]
  command-line interpreter for the PHP scripting language

php7.0-common/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed,automatic]
  documentation, examples and common module for PHP

php7.0-curl/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  CURL module for PHP

php7.0-dba/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  DBA module for PHP

php7.0-dev/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Files for PHP7.0 module development

php7.0-enchant/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Enchant module for PHP

php7.0-fpm/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  server-side, HTML-embedded scripting language (FPM-CGI binary)

php7.0-gd/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  GD module for PHP

php7.0-gmp/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  GMP module for PHP

php7.0-imap/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  IMAP module for PHP

php7.0-interbase/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Interbase module for PHP

php7.0-intl/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Internationalisation module for PHP

php7.0-json/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed,automatic]
  JSON module for PHP

php7.0-ldap/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  LDAP module for PHP

php7.0-mbstring/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  MBSTRING module for PHP

php7.0-mcrypt/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  libmcrypt module for PHP

php7.0-mysql/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  MySQL module for PHP

php7.0-odbc/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  ODBC module for PHP

php7.0-opcache/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed,automatic]
  Zend OpCache module for PHP

php7.0-pgsql/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  PostgreSQL module for PHP

php7.0-phpdbg/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  server-side, HTML-embedded scripting language (PHPDBG binary)

php7.0-pspell/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  pspell module for PHP

php7.0-readline/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed,automatic]
  readline module for PHP

php7.0-recode/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  recode module for PHP

php7.0-snmp/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  SNMP module for PHP

php7.0-soap/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  SOAP module for PHP

php7.0-sqlite3/yakkety-updates,yakkety-updates,now 7.0.13-0ubuntu0.16.10.1 amd64 [installed]
  SQLite3 module for PHP

php7.0-sybase/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Sybase module for PHP

php7.0-tidy/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  tidy module for PHP

php7.0-xml/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  DOM, SimpleXML, WDDX, XML, and XSL module for PHP

php7.0-xmlrpc/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  XMLRPC-EPI module for PHP

php7.0-xsl/yakkety-updates,yakkety-updates,yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 all
  XSL module for PHP (dummy)

php7.0-zip/yakkety-updates,yakkety-updates 7.0.13-0ubuntu0.16.10.1 amd64
  Zip module for PHP

php7cc/yakkety,yakkety 1.1.0-1 amd64
  command line tool to detect PHP 7 incompatible code

MySQL

Кто-то ставит MariaDB, возможно, ожидая подвоха от Oracle, но я как-то прикипел к MySQL:


# apt install mysql-server
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
libaio1 libcgi-fast-perl libcgi-pm-perl libencode-locale-perl libevent-core-2.0-5
libfcgi-perl libhtml-parser-perl libhtml-tagset-perl libhtml-template-perl
libhttp-date-perl libhttp-message-perl libio-html-perl liblwp-mediatypes-perl
libtimedate-perl liburi-perl mysql-client-5.7 mysql-client-core-5.7 mysql-common
mysql-server-5.7 mysql-server-core-5.7
Suggested packages:
libdata-dump-perl libipc-sharedcache-perl libwww-perl mailx tinyca
The following NEW packages will be installed:
libaio1 libcgi-fast-perl libcgi-pm-perl libencode-locale-perl libevent-core-2.0-5
libfcgi-perl libhtml-parser-perl libhtml-tagset-perl libhtml-template-perl
libhttp-date-perl libhttp-message-perl libio-html-perl liblwp-mediatypes-perl
libtimedate-perl liburi-perl mysql-client-5.7 mysql-client-core-5.7 mysql-common
mysql-server mysql-server-5.7 mysql-server-core-5.7
0 upgraded, 21 newly installed, 0 to remove and 0 not upgraded.
Need to get 20.3 MB of archives.
After this operation, 172 MB of additional disk space will be used.

Твикать mysql пока не будем, если нагрузки будут большими, тогда пусть голова болит.

Связываем Nginx с PHP, создаём сайты

Итак, мы установили полный стек LEMP, теперь будем создавать сайты, создадим 3 сайта, остальные добавляем по подобию.

Названия и адреса наших сайтов:
1. site.ru
2. site.org
3. site.com

Для каждого сайта создадим в каталоге /var/www одноименные с именем сайта папки: site.ru site.org site.com. В каждой из этих папок создадим три подпапки:

  • tmp - для сессий и загрузок файлов
  • log - для логов nginx
  • web - для www данных (само содержимое сайта)
cd /var/www
mkdir -p site.ru/{tmp,web,log}
mkdir -p site.org/{tmp,web,log}
mkdir -p site.com/{tmp,web,log}

Также для каждого сайта будем создавать своего юзера с именам web1, web2, web3 и т.д. (при добавлении 4-го сайта создадим юзера web4, см. ниже). для каждого юзера будем использовать группу www-data.

Итак, добавляем пользователей и устанавливаем права доступа на соотв. папки. Все команды напомню осуществляем под root'ом:

useradd -m -d /var/www/site.ru -s $(which bash) -G www-data web1
useradd -m -d /var/www/site.org -s $(which bash) -G www-data web2
useradd -m -d /var/www/site.com -s $(which bash) -G www-data web3
 
cd /var/www
chown web1:www-data -R site.ru
chown web2:www-data -R site.org
chown web3:www-data -R site.com
 
passwd web1
passwd web2
passwd web3

Запомним пароли для пользователей web1, web2 и web3, они пригодятся нам далее для доступа к сайтам через FTP (см. далее по тексту).

PHP конфиг

Создадим конфигурационные файлы PHP-FPM для каждого юзера/сайта. Каждый сайт у нас будет обслуживаться своими PHP-FPM процессами, чтобы распределить нагрузку по всем сайтам. Названия конф файлов web[1..3].conf, они находятся в папке /etc/php/7.0/fpm/pool.d, PHP-FPM при запуске подключает всё оттуда. Итак, пример конфигурации для сайта site.ru (юзера web1):

cd /etc/php/7.0/fpm/pool.d
vim web1.conf

Содержимое web1.conf:

[web1]

listen = /run/php/php7.0-fpm1.sock
listen.owner = web1
listen.group = www-data
listen.mode = 0660

user = web1
group = www-data

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 0

chdir = /

php_admin_value[open_basedir] = /var/www/site.ru/web:/var/www/site.ru/tmp
php_admin_value[session.save_path] = /var/www/site.ru/tmp
php_admin_value[upload_tmp_dir] = /var/www/site.ru/tmp
php_admin_value[sendmail_path] = "/usr/sbin/sendmail -t -i"

security.limit_extensions = .php .html

Отметьте параметры:

listen = /run/php/php7.0-fpm1.sock - указываем на каком сокете будет слушать процесс PHP-FPM, он нам пригодится для настройки NGINX (см. ниже).

listen.owner = web1
listen.group = www-data - владелец/группа прав доступа к сокету

user = web1
group = www-data - владелец/группа запущенного процесса при подключении к сокету

php_admin_value[open_basedir] - указываем, в какие папки будет иметь доступ владелец процесса
php_admin_value[session.save_path] - указываем, в какой папке будут храниться сессии
php_admin_value[upload_tmp_dir] - указываем, в какую папку будут скачиваться файлы (upload)
php_admin_value[sendmail_path] - команда отправки почты, иcпользуемая функцией PHP mail. Настройкой почты мы займёмся чуть позже.

Для остальных двух сайтов - копируем файл web1.conf и поменяем юзера (web1 -> web2 -> web3), сокет - /run/php/php7.0-fpm1.sock -> /run/php/php7.0-fpm2.sock -> /run/php/php7.0-fpm3.sock и папку site.ru -> site.org -> site.com

cd /etc/php/7.0/fpm/pool.d
cp web1.conf web2.conf
vim web2.conf
cp web2.conf web3.conf
vim web3.conf

Настраиваем NGINX

Настройки сайтов хранятся в папке /etc/nginx/sites-available - доступные сайты. Чтобы сделать сайт активным и видимым nginx на него делают мягкую ссылку (симлинк) отсюда в папку /etc/nginx/sites-enabled - активные сайты.

Итак, заходим в папку /etc/nginx/sites-available и создаём файл для настроек первого сайта. Имя файла конечно же может быть любым, мы будем делать имена файлов в виде имён сайтов с добавления суффикса .vhost, чтобы показать, что это настройки виртуального хоста. А симлинки будем назвать по имени используемого пользователя, чтобы видеть, каким именно пользователем мы используем данный сайт. Эта информация может пригодится, например, ч/з полгода, если мы давно не будем заглядывать сюда и у нас вылетит из головы, каких пользователей под какие сайты мы задавали - в этом случае будет не обязательно копаться в файлах настроек Nginx и PHP. Итак, приступим:

cd /etc/nginx/sites-available/
vim site.ru.vhost
ln -s site.ru.vhost /etc/nginx/sites-enabled/web1

Полное содержимое файла site.ru.vhost:

server {
        listen *:80;
        server_name site.ru www.site.ru;
        root   /var/www/site.ru/web;

        index index.html index.php;

        error_log /var/www/site.ru/log/error.log;
        access_log /var/www/site.ru/log/access.log combined;

        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }

        location = /favicon.ico {
            log_not_found off;
            access_log off;
        }

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|svg)$ {
            expires max;
            # log_not_found off;
				}

        location ~ \.(php|html)$ {
            try_files /d58f8ccd9bffa83ebec930554209111f.htm @php;
        }

        location @php {
            try_files $uri =404;
            include /etc/nginx/fastcgi_params;
            fastcgi_pass unix:/run/php/php7.0-fpm1.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_intercept_errors on;
        }
}

Бегло пробежимся по настройкам. Итак,

root /var/www/site.ru/web;

как мы и говорили вначале, корень сайтов будет лежать в папке web.

location ~ /\. {
deny all;
access_log off;
log_not_found off;
}

Этот блок говорит о том, чтобы закрыть доступ ко всем файлам и папкам, начинающимся с точки (`.`). Если вам это не нужно, этот блок можно удалить.

location @php {
try_files $uri =404;
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/run/php/php7.0-fpm1.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors on;
}

Этот блок связывает NGINX с PHP-FPM ч/з сокет /run/php/php7.0-fpm1.sock - именно этот сокет мы задавали в web1.conf в качестве параметра для listen.

Остальные сайты делаем по образу и подобию site.ru.vhost - копируем, меняем в файле имя сервера и корневой путь к сайту (параметры server_name и root), не забываем исправить пути в error_log и access_log, и главное - не забудем указать другой сокет в параметре fastcgi_pass - это будет unix:/run/php/php7.0-fpm2.sock для site.org и unix:/run/php/php7.0-fpm3.sock для сайта site.com. После создания файлов в папке sites-available делаем на них симлинки в папку sites-enabled:

cd /etc/nginx/sites-available/
cp site.ru.vhost site.org.vhost
vim site.org.vhost
ln -s site.org.vhost /etc/nginx/sites-enabled/web2
cp site.ru.vhost site.com.vhost
vim site.com.vhost
ln -s site.com.vhost /etc/nginx/sites-enabled/web3

После создания и каждого изменения конфигурационых файлов следует перезапускать соотв. службу. Мы меняли конфиги для PHP-FPM и NGINX, поэтому выполняем следующие две команды:

service nginx reload
service php7.0-fpm reload

Если нет ошибки в запуске служб, заходим на наши сайты и любуемся ими! Для проверки создадим файл 1.php с содержимым:

<?php 
phpinfo();

Закинем его в папку /var/www/site.ru/web.
Теперь при заходе на site.ru/1.php мы должны увидеть экран с текущими настройками PHP, типа такого:
phpinfo.png
Смотрим текущие параметры, подключенные модули PHP и убедждаемся в том, что всё вроде настроено как надо. Тоже самое повторим для сайтов site.org и site.com.

Ещё добавлю один нюанс - иногда организация нашего сайта требует, чтобы все запросы к сайту шли через один шлюз, обычно это index.php. То есть какой бы путь мы ни указали при обращении к сайту, управление будет передано именно index.php. В nginx есть несколько способ организовать подобное и я приведу способ, который используем мы для одного из наших сайтов.
Итак, вот конфиг NGINX для сайта, к которому все запросы (правда, кроме обращения к папке inc, в которой у нас хранятся публичные ресурсы - картинки, JavaScripts и CSS-файлы), идут напрямую index.php, а там мы уже решаем, что делать с данным запросом:

server {
        listen *:80;
        server_name site.com www.site.com;
        root   /var/www/site.com/web;

        index index.html index.php;

        error_log /var/www/site.com/log/error.log;
        access_log /var/www/site.com/log/access.log combined;

        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }

        location = /favicon.ico {
            log_not_found off;
            access_log off;
        }

        location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|svg)$ {
            expires max;
            # log_not_found off;
        }

        location ~ \.(php|html)$ {
            try_files /d58f8ccd9bffa83ebec930554209111f.htm @php;
        }

        location @php {
            try_files $uri =404;
            include /etc/nginx/fastcgi_params;
            fastcgi_pass unix:/run/php/php7.0-fpm3.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_intercept_errors on;
        }
        
        location /inc/ {}
        location / {
            rewrite ^ /index.php last;
        }
}

В этом конфиге самые интересные такие блоки:

location /inc/ {}
location / {
rewrite ^ /index.php last;
}

Первый блок приказывает nginx обрабатывать путь к папке inc как есть, а второй блок все запросы перенаправляет файлу index.php.

Добавляем 4-ый сайт: site2.ru - web4

cd /var/www
mkdir -p site2.ru{tmp,web,log}
useradd -m -d /var/www/site2.ru -s $(which bash) -G www-data web4
passwd web4
chown web4:www-data -R /var/www/site2.ru
 
cp /etc/php/7.0/fpm/pool.d/web3.conf /etc/php/7.0/fpm/pool.d/web4.conf
vim /etc/php/7.0/fpm/pool.d/web4.conf
cp /etc/nginx/sites-available/site.com.vhost /etc/nginx/sites-available/site2.ru.vhost
vim /etc/nginx/sites-available/site2.ru.vhost
ln -s /etc/nginx/sites-available/site2.ru.vhost /etc/nginx/sites-enabled/web4
service nginx reload
service php7.0-fpm reload

Настройка FTP сервера

Прежде чем приступить к настройкам почты, настроим FTP сервер, чтобы можно было заходить по FTP(FTPS) в папку сайта и обновлять его. Инсталлируем pure-ftpd:

# apt install pure-ftpd
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
pure-ftpd-common
The following NEW packages will be installed:
pure-ftpd pure-ftpd-common
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 263 kB of archives.
After this operation, 796 kB of additional disk space will be used.
Do you want to continue? [Y/n] y

Настройка Pure-ftpd осуществляется оригинально - надо в папке /etc/pure-ftpd/conf создать файл с именем параметра и вписать требуемое значение в его содержимое. Странно, но ладно. Итак, давайте создадим исполняемый скрипт, который будет задавать нужные нам настройки и потом запустим его:

vim /root/pureftpd-conf.sh
#!/bin/sh
echo 'yes' > ChrootEveryone 
echo '50' > MaxClientsNumber 
echo '10' > MaxClientsPerIP 
echo 'no' > VerboseLog 
echo 'yes' > DisplayDotFiles 
echo 'no' > ProhibitDotFilesWrite 
echo 'yes' > NoChmod 
 echo 'yes' > NoAnonymous 
echo 'yes' > DontResolve 
echo '15' > MaxIdleTime
# 1 - simple or TLS, 2 - TLS only 
echo '2' > TLS
# allow unux users FTPing
echo 'yes' > UnixAuthentication 
echo '1000' > MinUID

Кратко описание настроек: мы делаем chroot для всех, чтобы не лазали по всему нашему серверу, отображаем файлы начинающиеся с точки (в Юникс это типа скрытые файлы) и разрешаем их читать/писать, запрещаем анонимный вход, задаём 15 минут макс. возможного простоя, также обязуем всех клиентов использовать TLS (шифрование) - строка echo '2' > TLS. Конечно, рекомендуется всегда где возможно использовать шифрование, чтобы MIM (men in the middle) не могли перехватывать наши файлы. То есть перехватывать они смогут по-любому, но расшифровать - вряд ли. Недостаток шифрования - более медленная скорость передачи файлов, т.к. тратится время на шифровку/дешифровку. Чтобы полностью отключить шифрование, надо в TLS вписать 0, чтобы разрешить обычные и шифрованный трафик - сюда вписать единичку (1).
Также важен последний параметр echo 'yes' > UnixAuthentication. Помните юзеров, которые мы создавали в связке PHP - NGINX (web1, web2 web3 и т.д.) Теперь мы можем использовать их также в качестве FTP-юзеров. Используем теже пароли, что мы задавали при их создании.

Для того, чтобы использовать TLS, создадим самоподписной сертификат:

# openssl req -x509 -nodes -days 7300 -newkey rsa:2048 -keyout /etc/ssl/private/pure-ftpd.pem -out /etc/ssl/private/pure-ftpd.pem

После всех манипуляций перезапустим наш демон:

# service pure-ftpd restart

PureFTPD пишет логи в syslog. Смотреть:

# tail -f /var/log/syslog

Всё, теперь мы можем соеднияться по FTP(S) (S - secure), указывая пользователей web1,web2, web3, при этом они по умолчанию будут попадать в свои домашние папки, которые являются папками их сайтов.

Настройка почты - Postfix + Dovecot

Итак, у нас есть работающию сайты и FTP доступ к ним, чтобы заливать/обновлять/удалять контент на этих сайтах. Также мы можем работать с базами данных MySQL/SQlite.

Для полноценной работы нам осталось настроить почту, чтобы отсылать/получать письма на своих сайтах или в своей системе. Система почты в общем случае состоит из двух компонентов MTA - mail transer agent, агент передачи почты и MDA - mail delivery agent, агент доставки почты. В качестве золотой пары мы будем использовать Postfix + Dovecot, как наиболее популярные на сегодня, и не зря, по-моему скромному мнению. Итак, инсталлируем пакеты, настраиваем конфиги и создаём почтовые домены и ящики. Поехали!

Шаг 1: Инсталлируем пакеты и устанавливаем сертификат

# apt install postfix postfix-mysql dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql dovecot-pop3d
В диалоговом окне указываем : Internet Site, site.ru
Создаём ключ/сертификат dovecot.key и dovecot.pem:
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/dovecot.key -out /etc/ssl/certs/dovecot.pem
Мои ответы на вопросы в порядке их возникновения: RU Volgograd Volgograd Justbeo CEO beotiger mymail@gmail.com

Проверяем доступность наших доменов для почты:
# dig MX site.ru +short @ns1.he.net
10 mail.site.ru.
# host mail.site.ru ns1.he.net

Using domain server:
Name: ns1.he.net
Address: 216.218.130.2#53
Aliases:

mail.site.ru has address 131.21.128.229

Шаг 2: Создаём базу данных MySQL, виртуальные домены, пользователей и альясы

Создадим особую БД для хранения наших виртуальных доменов, пользователей и альясов. Назовём её к примеру vmail. В этой базе создадим виртуальные домены под все наши 3 сайта, а также двух пользователя для двух сайтов:

# mysqladmin -p create vmail
mysql > GRANT SELECT ON vmail.* TO 'vmail'@'127.0.0.1' IDENTIFIED BY 'password';
mysql > FLUSH PRIVILEGES;
mysql> USE vmail;
mysql> CREATE TABLE `domains` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

mysql> CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT,
`domain_id` INT NOT NULL,
`password` VARCHAR(106) NOT NULL,
`email` VARCHAR(120) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

mysql> CREATE TABLE `aliases` (
`id` INT NOT NULL AUTO_INCREMENT,
`domain_id` INT NOT NULL,
`source` varchar(100) NOT NULL,
`destination` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- Наши виртуальные домены. id по умолчанию начинается с 1:
mysql> INSERT INTO `domains` (`name`)
VALUES ('site.ru'), ('site.org'), ('site.com');

mysql> INSERT INTO `users` (`domain_id`, `password` , `email`)
VALUES
('1', ENCRYPT('pass1', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'user1@site.ru'),
('2', ENCRYPT('pass2', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'user2@site.org');

-- добавляем ещё одного пользователя для site.com, не забываем про правильный domain_id:
mysql> INSERT INTO `users` (`domain_id`, `password` , `email`)
VALUES ('3', ENCRYPT('pass3', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'user3@site.com');

-- Note: Warning | 1287 | 'ENCRYPT' is deprecated and will be removed in a future release. Please use AES_ENCRYPT instead

-- добавляем альяс info@site.ru, который будет ссылаться на user1@site.ru, то есть письмо на info@site.ru уйдёт пользователю user1@site.ru. Важно - следует свериться с ид домена из таблицы domains, чтобы поле domain_id ему соответствовало:
mysql> INSERT INTO `aliases` (`domain_id`, `source`, `destination`)
VALUES ('1', 'info@site.ru', 'user1@site.ru');

Аналогично добавляем другие нужные альясы. Чтобы добавить глобальный альяс, который будет пересылать письма, уходящие на любой неопределенный адрес выбранного домена определенному пользователю данного домена:
mysql> INSERT INTO `aliases` (`domain_id`, `source`, `destination`)
VALUES ('1', '@site.ru', 'user1@site.ru');

-- завершаем сеанс работы с MySQL:
QUIT;

Шаг 3: Конфигурируем Postfix

Основные настройки Postfix находятся в двух файлах - main.cf и master.cf. В master.cf можно переопределять некоторые настройки для определённых служб (флаг -o - override, переопределить). Начнём с main.cf. Кратко. что мы тут делаем: используем шифрованние TLS, виртуальные домены и пользователей ч/з MySQL базу, Dovecot. переопределяем только указанные ниже настройки, остальные оставляем по умолчанию, как они были в main.cf изначально:

# cp /etc/postfix/main.cf /etc/postfix/main.cf.orig
# vim /etc/postfix/main.cf
# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/dovecot.pem
smtpd_tls_key_file=/etc/ssl/private/dovecot.key
smtpd_use_tls=yes
smtpd_tls_auth_only = yes

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination

# mydestination = $myhostname, site.com, localhost.com, , localhost
mydestination = localhost, localhost.localdomain
myhostname = site.com
## Tells Postfix to use Dovecot's LMTP instead of its own LDA to save emails to the local mailboxes.
virtual_transport = lmtp:unix:private/dovecot-lmtp
## Tells Postfix you're using MySQL to store virtual domains, and gives the paths to the database connections.
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf

Создаём нужные файлы:

# vim /etc/postfix/mysql-virtual-mailbox-domains.cf
user = vmail
password = gR29eZ34
hosts = 127.0.0.1
dbname = vmail
query = SELECT 1 FROM domains WHERE name='%s'

# vim /etc/postfix/mysql-virtual-mailbox-maps.cf
user = vmail
password = gR29eZ34
hosts = 127.0.0.1
dbname = vmail
query = SELECT 1 FROM users WHERE email='%s'

# vim /etc/postfix/mysql-virtual-alias-maps.cf
user = vmail
password = gR29eZ34
hosts = 127.0.0.1
dbname = vmail
query = SELECT destination FROM aliases WHERE source='%s'

Проверка:

service postfix restart
postmap -q site.ru mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
postmap -q user2@site.org mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
postmap -q info@site.ru mysql:/etc/postfix/mysql-virtual-alias-maps.cf

Первые две проверки должны вернуть 1, третья - email для альяса: user1@site.ru

vim /etc/postfix/master.cf


submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject

Проверим все установки Postfix и сделаем рестарт его (релоада недостаточно что ли?):

postconf -n
service postfix restart

Шаг 4: Конфижим Dovecot

Создаём копии на всякий случай:

cp /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.orig
cp /etc/dovecot/conf.d/10-mail.conf /etc/dovecot/conf.d/10-mail.conf.orig
cp /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.orig
cp /etc/dovecot/dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext.orig
cp /etc/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf.orig
cp /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.orig
 
vim /etc/dovecot/dovecot.conf


...
!include_try /usr/share/dovecot/protocols.d
protocols = imap lmtp pop3

vim /etc/dovecot/conf.d/10-mail.conf


mail_location = maildir:/var/mail/vhosts/%d/%n
mail_privileged_group = mail

Создание юзера, папок и проверка доступа
Введем команду:

ls -ld /var/mail

Убедимся, что параметры доступа таковы:

drwxrwsr-x 3 root vmail 4096 Jan 24 21:23 /var/mail

Создаём папки для каждого домена из MySQL таблицы domains

mkdir -p /var/mail/vhosts
cd /var/mail/vhosts
mkdir site.com site.org site.ru

Создаем юзера и группу vmail с id = 5000

groupadd -g 5000 vmail 
useradd -g vmail -u 5000 vmail -d /var/mail

И меняем на него владение папки /var/mail:

chown -R vmail:vmail /var/mail

Теперь правим файл /etc/dovecot/conf.d/10-auth.conf

vim /etc/dovecot/conf.d/10-auth.conf


disable_plaintext_auth = yes
auth_mechanisms = plain login
#!include auth-system.conf.ext
!include auth-sql.conf.ext

Т.е. поменяли авторизацию с системной на MySQL.

vim /etc/dovecot/conf.d/auth-sql.conf.ext


passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
}

vim /etc/dovecot/dovecot-sql.conf.ext


driver = mysql
connect = host=127.0.0.1 dbname=vmail user=vmail password=gR29eZ34
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM users WHERE email='%u';

chown -R vmail:dovecot /etc/dovecot
chmod -R o-rwx /etc/dovecot
vim /etc/dovecot/conf.d/10-master.conf


##Uncomment inet_listener_imap and modify to port 0
service imap-login {
inet_listener imap {
port = 0
}

#Create LMTP socket and this configurations
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
#inet_listener lmtp {
# Avoid making LMTP visible for the entire internet
#address =
#port =
#}
}

# Modify unix_listener parameter to service_auth like this:

service auth {

unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}

unix_listener auth-userdb {
mode = 0600
user = vmail
#group =
}

#unix_listener /var/spool/postfix/private/auth {
# mode = 0666
#}

user = dovecot
}

service auth-worker {
# Auth worker process is run as root by default, so that it can access
# /etc/shadow. If this isn't necessary, the user should be changed to
# $default_internal_user.
user = vmail
}

vim /etc/dovecot/conf.d/10-ssl.conf


ssl = required
ssl_cert =

Посмотреть полную конфигурацию dovecot можно командой:

dovecot -n
service dovecot restart
telnet site.com 993 # проверка открытого порта 993 (IMAP)
telnet site.com 995 # проверка открытого порта 995 (POP3)
telnet site.com 587 # проверка открытого порта 587 (SMTP)

Поздравляю! Мы успешно настроили наш почтовый сервер и теперь можно приступать к тестам!


- Username: user2@site.org
- Password: pass2
- IMAP: site.org:993
- SMTP: site.org:587

Примечание: используем порты 993 для secure IMAP, 587 или 25 для SMTP и 995 для POP3?
Настройки почтовика:
Send: "SMTP Auth automatic" "Use SSL/TLS" "Use STARTTLS command to start encrypted session" "SMTP port: 587"

Шаг 5: Добавляем и конфигурирем SpamAssassin

Инсталлируем SpamAssassin


# apt install spamassassin spamc -- a spamc зачем?
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
binutils cpp cpp-6 dirmngr gcc gcc-6 gnupg gnupg-agent gnupg-l10n libasan3 libatomic1
libc-dev-bin libc6-dev libcc1-0 libcilkrts5 libdigest-hmac-perl libgcc-6-dev
libio-socket-inet6-perl libisl15 libitm1 liblsan0 libmail-spf-perl libmpc3 libmpx2
libnet-dns-perl libnet-ip-perl libnetaddr-ip-perl libquadmath0 libsocket6-perl
libsys-hostname-long-perl libtsan0 libubsan0 linux-libc-dev make manpages manpages-dev
pinentry-curses re2c sa-compile
Suggested packages:
binutils-doc cpp-doc gcc-6-locales tor gcc-multilib autoconf automake libtool flex bison gdb
gcc-doc gcc-6-multilib gcc-6-doc libgcc1-dbg libgomp1-dbg libitm1-dbg libatomic1-dbg
libasan3-dbg liblsan0-dbg libtsan0-dbg libubsan0-dbg libcilkrts5-dbg libmpx2-dbg
libquadmath0-dbg parcimonie xloadimage glibc-doc make-doc pinentry-doc razor
libio-socket-ssl-perl libdbi-perl pyzor libmail-dkim-perl libencode-detect-perl
The following NEW packages will be installed:
binutils cpp cpp-6 dirmngr gcc gcc-6 gnupg gnupg-agent gnupg-l10n libasan3 libatomic1
libc-dev-bin libc6-dev libcc1-0 libcilkrts5 libdigest-hmac-perl libgcc-6-dev
libio-socket-inet6-perl libisl15 libitm1 liblsan0 libmail-spf-perl libmpc3 libmpx2
libnet-dns-perl libnet-ip-perl libnetaddr-ip-perl libquadmath0 libsocket6-perl
libsys-hostname-long-perl libtsan0 libubsan0 linux-libc-dev make manpages manpages-dev
pinentry-curses re2c sa-compile spamassassin spamc
0 upgraded, 41 newly installed, 0 to remove and 0 not upgraded.
Need to get 30.0 MB of archives.
After this operation, 119 MB of additional disk space will be used.
Do you want to continue? [Y/n] yes?

Устанавливаем права доступа

Добавляем группу spamd:

# groupadd spamd

потом пользователя spamd с домашней директорией /var/log/spamassassin:

# useradd -g spamd -s /bin/false -d /var/log/spamassassin spamd

и затем создаём папку /var/log/spamassassin:

# mkdir /var/log/spamassassin

Меняем права доступа к этой папке на spamd:

# chown spamd:spamd /var/log/spamassassin

Займемся непосредственно конфигурацией


# vim /etc/default/spamassassin

Меняем параметр ENABLED для включения самого демона SpamAssassin.

ENABLED=1

Также изменим домашнюю папку и опции:

OPTIONS="--create-prefs --max-children 3 --username spamd -H /var/log/spamassassin/ -s /var/log/spamassassin/spamd.log"

И включаем крон для автоматического апдейта правил SpamAssassin:

CRON=1

Далее открываем файл /etc/spamassassin/local.cf и устанавливаем нужные нам правила анти-спама:

# vim /etc/spamassassin/local.cf

SpamAssassin ставит баллы каждому письму, и если этот балл превысит определенное значение, в нашем примере 5.0, тогда это письмо будет считаться спамом. Можно использовать такие параметры для анти-спам правил:

rewrite_header Subject *** SPAM _SCORE_ ***
report_safe 0
required_score 5.0
use_bayes 1
use_bayes_rules 1
bayes_auto_learn 1
skip_rbl_checks 0
use_razor2 0
use_dcc 0
use_pyzor 0

Теперь меняем один из конфигов Postfix /etc/postfix/master.cf для указания того, что каждое поступающее письмо должно проходить проверку нашим SpamAssassin.

# vim /etc/postfix/master.cf

Находим следующую строку и добавляем фильтр для spamassassin:


smtp inet n - - - - smtpd
-o content_filter=spamassassin

В конце надо будет добавить следующие параметры:

spamassassin unix - n n - - pipe
user=spamd argv=/usr/bin/spamc -f -e
/usr/sbin/sendmail -oi -f ${sender} ${recipient}

Теперь нужно перезапустить наши службы, у которых мы меняли конфиги:

# service spamassassin restart
# service postfix restart

Всё, мои поздравления! Мы успешно сконфижили Postfix с поддержкой SpamAssassin!

Ага, а тесты?!

Тест для SpamAssassin: создайте и пошлите себе письмо со стороннего сервера со следующей темой:

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

И ещё можно выполнить следующую команду:

# spamassassin -D < /usr/share/doc/spamassassin/examples/sample-spam.txt

FAIL2BAN

Эта простая, но мощная штука позволяет банить ботов/злоумышленников по ip-адресам на определённые сроки.
Устанавливается и настраивается буквально за пару минут. Правила для большинства служб в ней уже прописаны по умолчанию, а если нам надо добавить что-то своё, это делается на основе регулярок.

Правила прописываются в файле jail.conf, но он может измениться при очередном апгрейде fail2ban, поэтому рекомендуется вносить правила в файл jail.local, который имеет приоритет перед jail.conf.

Итак, приступим, установим пакет и пропишем правила для наших служб:

apt install fail2ban
vim /etc/fail2ban/jail.local
[DEFAULT]
bantime = 600
findtime = 600
maxretry = 3

destemail = admin@site.ru
sender = fail2ban@site.ru
sendername = Fail2Ban
mta = sendmail

action = %(action_mw)s

[sshd]
enabled  = true
port    = 11974
bantime = 3600
logpath  = /var/log/auth.log

[postfix]
enabled  = true
logpath  = /var/log/mail.log
bantime  = 7200

[dovecot]
enabled  = true
logpath  = /var/log/mail.log
findtime = 300
bantime  = 1800

[postfix-sasl]
enabled  = true
logpath  = /var/log/mail.log
findtime = 300
maxretry = 10
bantime  = 1800

FAIL2BAN использует линунксовый файервол для фильтрации и бана.
Посмотрим, какие правила у нас есть перд стартом службы, и какие добавились после её запуска
(замечу, что файревол перед этим мы не настраивали):

service fail2ban stop
iptables -S


-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

service fail2ban start
iptables -S


-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N f2b-dovecot
-N f2b-postfix
-N f2b-postfix-sasl
-N f2b-sshd
-A INPUT -p tcp -m multiport --dports 110,995,143,993,587,465,4190 -j f2b-dovecot
-A INPUT -p tcp -m multiport --dports 25,465,587,220,993,110,995 -j f2b-postfix-sasl
-A INPUT -p tcp -m multiport --dports 25,465,587 -j f2b-postfix
-A INPUT -p tcp -m multiport --dports 11974 -j f2b-sshd
-A f2b-dovecot -j RETURN
-A f2b-postfix -j RETURN
-A f2b-postfix-sasl -j RETURN
-A f2b-sshd -j RETURN

5 минут спустя получаем 2 адреса в бане:

iptables -S


...
-A f2b-postfix-sasl -s 79.129.39.26/32 -j DROP
-A f2b-postfix-sasl -s 181.214.206.133/32 -j DROP
...

Проверка статуса службы:

systemctl status fail2ban.service


● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2018-05-06 14:29:15 MSK; 17h ago
Docs: man:fail2ban(1)
Main PID: 1417 (fail2ban-server)
Tasks: 9
Memory: 19.7M
CPU: 35.827s
CGroup: /system.slice/fail2ban.service
└─1417 /usr/bin/python3 /usr/bin/fail2ban-server -s /var/run/fail2ban/fail2ban.sock -p /var/run/fai

Вот и всё, теперь о Fail2Ban, равно как и о ботах, ломящихся на наш сервер, можно забыть. Но расслабляться всё равно не стоит.

Финал

Вот в принципе и всё, конец поста, кто прочёл всё это сначала до конца, без пролистывания, и ещё что-нибудь понял и усвоил без несварения желудка, тому респект и уважуха, всем остальным - чаю, пока! 0_0 ^_^

Создаём плэйлисты (playlists) M3U/PLS на PHP

Итак, у нас имеется список треков в формате MP3 в нескольих папках.
Мы хотим создать плэйлисты в формате M3U/PLS для этих треков.
Информации по данным форматам не особо много, она пересекается (согласно гуглу, конечно), но в итоге можно использовать то, что мы прочитаем в википедии:
Описание формата M3U – https://ru.wikipedia.org/wiki/M3U. Для него конечно же будем использовать Extended M3U и формат M3U8 – в кодировке UTF-8.
описание формата PLS в русскоязычной ВИКИ ужасное, сами убедитесь: https://ru.wikipedia.org/wiki/PLS_(%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0). Поэтому будем отталкиваться от английской версии – https://en.wikipedia.org/wiki/PLS_(file_format) Там тоже описание сумбурное какое-то, но в принципе, понятное. Это же ВИКИ, что вы хотели?!
Continue reading

Комментарии VKontakte/Facebook – перехватываем и отправляем нам на почту

Добавляем виджеты комментариев VK/FB на свою страничку

Создаём приложения

Добавить комменты VK/FB на свой сайт относительно просто – создаём соотв. приложения в VK/FB, получаем их id. Вот адреса страничек для создания приложений: https://vk.com/dev/widgets_for_sites для ВК и https://developers.facebook.com/apps/ для Фэйсбук.

Подробно на этой теме останавливаться нет смысла, так как у них меняется всё очень часто, я имею в виду всякую воду и интерфейс, и каждый раз вникать в одно и тоже под другим соусом не особенно хочется. Но принцип видимо остаётся одним и тем же, и созданное нами несколько лет назад приложения продолжают работать. Честно говоря, назвать приложением это от FB/VK у меня язык с трудом поворачивается, скорее это надстройка над виджетами, контроллер виджетов точнее. Главное – они обеспечивают связь нашего сайта с многотысячной армией пользователей этих популярных сетей.
Continue reading

Часовые пояса и расширение tar.lz в Windows

Здравствуйте!

Нам понадобилось вывести список мировых временных зон, чтобы можно было выбрать одну из них.
Для PHP в итоге я смог сделать такой вариант:

function getOptTimeZones($dk_timezone = 0)
{
	// построим select для dk_timezone
	$dk_tzs = array(
-15 => '(GMT -12:00) Эневеток, Кваджалейн',
-14 => '(GMT -11:00) Остров Мидуэй, Самоа',
-13 => '(GMT -10:00) Гавайи',
-12 => '(GMT -9:00) Аляска',
-11 => '(GMT -8:00) Тихоокеанское время (США и Канада), Тихуана',
-10 => '(GMT -7:00) Горное время (США и Канада), Аризона',
-9 => '(GMT -6:00) Центральное время (США и Канада), Мехико',
-8 => '(GMT -5:00) Восточное время (США и Канада), Богота, Лима',
-7 => '(GMT -4:00) Атлантическое время (Канада), Каракас, Ла Пас',
-6 => '(GMT -3:00) Бразилия, Буэнос-Айрес, Джорджтаун',
-5 => '(GMT -2:00) Среднеатлантическое время',
-4 => '(GMT -1:00) Азорские острова, острова Зелёного Мыса',
-3 => '(GMT  0:00) Дублин, Лондон, Лиссабон, Касабланка, Эдинбург',
-2 => '(GMT +1:00) Брюсель, Копенгаген, Мадрид, Париж, Берлин',
-1 => '(GMT +2:00) Афины, Киев, Минск, Бухарест, Рига, Таллин',
0 => '(GMT +3:00) Москва, Санкт-Петербург, Волгоград',
1 => '(GMT +4:00) Абу-Даби, Баку, Тбилиси, Ереван',
2 => '(GMT +5:00) Екатеринбург, Исламабад, Карачи, Ташкент',
3 => '(GMT +6:00) Омск, Новосибирск, Алма-Ата, Астана',
4 => '(GMT +7:00) Красноярск, Норильск, Бангкок, Ханой, Джакарта',
5 => '(GMT +8:00) Иркутск, Пекин, Перт, Сингапур, Гонконг',
6 => '(GMT +9:00) Якутск, Токио, Сеул, Осака, Саппоро',
7 => '(GMT +10:00) Владивосток, Восточная Австралия, Гуам',
8 => '(GMT +11:00) Магадан, Сахалин, Соломоновы Острова',
9 => '(GMT +12:00) Камчатка, Окленд, Уэллингтон, Фиджи');
 
	$s = '';
 
	for($i = -15; $i < 10; $i++) {
		$s .= '<option value="' . $i . '"';
		if($i == $dk_timezone)
			$s .= ' selected';
		$s .= '>' . $dk_tzs[$i] . '</option>';
	}
 
        return $s;
}

То есть мы построили список из option элементов для select‘а для сдвига от МСК. Т.о. (GMT +3:00) Москва, Санкт-Петербург, Волгоград у нас будет иметь значение 0, а всё остальное плюс-минус от этого значения. Переменная $dk_timezone содержит текущий сдвиг, что позвлояет выделить нужный option. Полученный результат надо облечь в select элемент.

Да, я знаю, что построил массив излишне (можно было только указать -15 вначале, но так нагляднее и интересней выглядит.

При поиске готового архивчика с таким зонами я часто натыкался на ссылку http://www.iana.org/time-zones, где якобы представлна вся инфо по теме.

На этом уважаемом сайте действитльено даётся свободная скачка трёх интресных файликов:
Latest version
2016j (Released 2016-11-23)
File Description
tzdb-2016j.tar.lz (380.4kb) Complete Distribution (Data, Code and Extras)
tzdata2016j.tar.gz (313.7kb) Data Only Distribution
tzcode2016j.tar.gz (203.8kb) Code Only Distribution

Заметьте, что два файлика имеют расширение tar.gz, и один, самый интересный, за каким-то лешим имеет расширение .tar.lz. я не знаю, что творится в голове администратора данного ресурса, но выглядит это забавно. Что стукнуло ему в голову самый нужный файлик облечь в расширение .tar.lz, вместо доброго старого .tar.gz или на худой конец такого же доброго и такого же старого и проверенного временем tar.bz2?

Я сейчас пока вишу в Виньде (Windows 10), для распаковки пользуюсь 7zip (http://www.7-zip.org/) – огромная благодарность Игорю Павлову со-товарищи за разработку столь замечательного продукта. tar.gz и tar.bz2 (как и многие другие архивы) он щёлкает как семечки, а вот об tar.lz (по крайней мере на текущий момент, что я сейчас пишу) обламывает свои острые мощные зубки:

7zip fails to open tar.lz archive

Я даже проапгрейдил свой 7zip до версии 16.04, топовой на сегодня – 2016-11-23, т.е. 23 ноября 2016 РХ.

Быстрый гугл вывел на зверя lzip, я где-то нарыл скомпиленную под винду версию – lzip.exe, быстро запустил его в терминале,и вот что он выдаёт, собака:


С:\>lzip -d tzdb.tar.lz
tzdb.tar.lz: file ends unexpectedly at pos 21
lzip: Deleting output file `tzdb.tar', if it exists.
lzip: WARNING: deletion of output file (apparently) failed.

Я перименовал яновский tzdb-2016j.tar.lz в tzdb.tar.lz

Итак, что же делать? Далее гуглим и находим такую страничку: http://mm.icann.org/pipermail/tz/2016-September/024112.html

Вкратце, там человек по имени Oscar van Vlijmen жалуется, что не может открыть файлик lz в windows, такой же бедолага как и я, и упоминает тот же lzip, который я только что описал чуть выше.

Зачем, зачем, зачем админы http://www.iana.org/time-zones запаковали lzip‘ом только один файлик, а не все три тогда уж? Что у них, повторяюсь в третий раз, в головах творится?

Итак, кто-то ему отвечает:

Q: Why did the tachyon cross the road?
A: Because it was on the other side.

К чему это? Сколько лет ответившему так? он считает себя самым умным или самым остроумным? нет слов.

Следующий чуть лучше отвечает Оскару:

> do we get a decent application program with a graphical user interface
You mean, like the other tools we’re using? Like ‘tar’, ‘make’, and ‘cc’? 🙂

С одной стороны он прав,а с другой нет. Если ты выкладываешь файл для всего мира на скачку без явного уклона в одну систему, сделай его максимально доступным для мира, хотя бы для наиболее распространенных систем.

Ладно, этот же человек подсказал, что в CygWin‘е есть lzip, сейас его попробую, он скачивает кучу зависимостей за собой, что обычно для Unix-way. Спасибо ему, всё же он не так плох, как я думал вначале. 🙂 шутка

О, урра, получилось!!

Итак, по шагам, как я распаковал tzdb-2016j.tar.lz с сайта http://www.iana.org/time-zones в Windows 10:

1. Инсталлируем CygWin (https://cygwin.com/install.html), я это сделал в папку E:\cygwin64 – скачиваем setup-x86_64.exe файлик, запускаем его, выбираем папки (e:\cygwin64 для самого Cygwin‘а и e:\cygwin64\_local\ для временных локальных фалов.

2. Далее ищем в фильтре lzip, выбираем пакеты (я выбрал оба пакета, хотя боюсь можно выбрать только сам lzip), нажимаем Next или Далее, он выдаст окошко с зависимостями, которые по умолчанию все выбраны, нажимаем ОК, далее или что там ещё, не помню, и минут через 5 инсталлятор закончит работу.

3. Кидаем в папку e:\cygwin64\home\ВашеИмя\ нужный архивчик (tzdb-2016j.tar.lz)

4. Запускаем e:\cygwin64\Cygwin.bat, делаем ls, чтобы убедиться, что он тут:

$ ls
tzdb-2016j.tar.lz

И даём команду на его распаковку в tar:


$ lzip -d tzdb-2016j.tar.lz

На экран ничего не вывелось, кроме нового приглашения, что для Линукса говорит о том, что всё прошло ОК (хотя это не всегда так есть), т.к. если бы была какая-то ошибка, нам бы об этом с радостью сообщили.

Смотрим ещё ls:

$ ls
tzdb-2016j.tar

7zip fails to open tar.lz archive

О, у нас появился tar архивчик. Пробуем открыть его уже штатными виндовыми методами, через 7zip, и он отлично открывается, показывая список файлов.

Всё, задача решена, можно пойти выпить чая или какао или на худой конец коньяка с кофе. Или кофе без коньяка, до НГ, точнее до 12:00 31 декабря 2016 года я спиртного не потребляю, печень берегу и голову.

На сим кланяюсь. Доброй ночи!

P.S. Да, в итоге, в этом архивчике много всего интересного, но то, что мне нужно – список часовых поясов со сдвигом GMT+- для простого пользователя не оказалось!

Потом я в архиве какого-то форума нашёл приведенный выше (в самом начале поста) список, быстро переделал его под PHP, убрал +-30 минут кое-где, т.к. они мне не были нужны. И так получилась функция getOptTimeZones.

Читаем/пишем бинарники на JavaScript (JScript) для Win Wscript

Да, проблема в том, что JavaScript пытается преобразовать полученные байты в юникод (unicode), и портит большинство байт, которые больше 0x7F. Поэтому при чтении/записи бинарников получается не то, что мы хотим.

Пошарив какое-то время в инете, я нашёл табличку преобразований символов больше либо равным 0x80, но она в итоге оказалась не полной, и мои бинарники читались и писались с ошибками.

В итоге я решил составить свои таблички для преобразования байт. Но вручную искать и записывать байты из – в мне показалось утомительным, поэтому я написал пару небольших скриптов.

Идея такая – создать бинарный файл, состоящий из последовательности байт от 0 до 255, то есть размером 256 байт, а потом распрасить его JavaScript’ом и проанализировать полученные символы.

Пишем краткий сценрий на PHP, ибо PHP всегда под рукой, что в Виньде ,что в Линуксе, что на серверах.

<?php
/* create file of bytes from 0 to 255 */
	$s = '';
	for($i = 0; $i < 256; $i++)
		$s .= chr($i);
 
	file_put_contents('256.dat', $s);

В итоге, мы имеем файл 256.dat, который проанализируем JavaScript’ и составим табличку преобразований на лету.

var name = '256.dat', i; // путь к нашему файлу и переменная д/цикла
 
/* ******************************************
* Используем API AkelPad's Scripts плагина  *
******************************************* */
var bytes = AkelPad.ReadFile(name, 0);	// binary
log('bytes.length=' + bytes.length); // должно быть 256
 
var abytes = bytes.split('');
log('abytes.length=' + abytes.length); // должно быть 256
 
var ascii = '', fromAscii = '', c;
for(i = 0; i < abytes.length; i++) {
        // table asciiCodeAt
        if(i > 127)
	  ascii += 'case 0x' + bin2hex(abytes[i]) +
            ': return 0x' + i.toString(16) + '; break;\n'
	// REVERSE TABLE fromAscii
	c = bin2hex(abytes[i]);
	if(c.length == 3)
		c = '0' + c;
	else
	if(c.length == 2)
		c = '00' + c;
	// count only high values
	if(i > 127)
	  fromAscii += 'case 0x' + i.toString(16) + ': s += \'\\u' +
            c.toUpperCase() + '\'; break;\n'
}
 
log('// ASCII:');
log(ascii)
log('// fromAscii:');
log(fromAscii);
 
function bin2hex(s) {
  //  discuss at: http://phpjs.org/functions/bin2hex/
  // original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // bugfixed by: Onno Marsman
  // bugfixed by: Linuxworld
  // improved by: ntoniazzi (http://phpjs.org/functions/bin2hex:361#comment_177616)
  //   example 1: bin2hex('Kev');
  //   returns 1: '4b6576'
  //   example 2: bin2hex(String.fromCharCode(0x00));
  //   returns 2: '00'
 
  var i, l, o = '',
    n;
 
  s += '';
 
  for (i = 0, l = s.length; i < l; i++) {
    n = s.charCodeAt(i)
      .toString(16);
    o += n.length < 2 ? '0' + n : n;
  }
 
  return o;
}
 
// Логируем текст с помощью AkelPad'овского Log плагина
function log(text)
{
  AkelPad.Call("Log::Output", 4, text + '\n');
}

Потом копируем полученный вывод в отдельный файл, добавляем необохдимую разметку и получаем в итоге полные таблицы преобразований to ASCII и from Ascii Code для JavaScript:

/*
	Needed for reading from binary files
 	See actionUtils.getImageSize method in emmet-app.js
*/
String.prototype.asciiCodeAt = function(i) {
    // charCodeAt returns some bytes translated to unicode characters. 
    // this function means to counteract that.
    switch(this.charCodeAt(i)) {
        case 0x402: return 0x80; break;
				case 0x403: return 0x81; break;
				case 0x201a: return 0x82; break;
				case 0x453: return 0x83; break;
				case 0x201e: return 0x84; break;
				case 0x2026: return 0x85; break;
				case 0x2020: return 0x86; break;
				case 0x2021: return 0x87; break;
				case 0x20ac: return 0x88; break;
				case 0x2030: return 0x89; break;
				case 0x409: return 0x8a; break;
				case 0x2039: return 0x8b; break;
				case 0x40a: return 0x8c; break;
				case 0x40c: return 0x8d; break;
				case 0x40b: return 0x8e; break;
				case 0x40f: return 0x8f; break;
				case 0x452: return 0x90; break;
				case 0x2018: return 0x91; break;
				case 0x2019: return 0x92; break;
				case 0x201c: return 0x93; break;
				case 0x201d: return 0x94; break;
				case 0x2022: return 0x95; break;
				case 0x2013: return 0x96; break;
				case 0x2014: return 0x97; break;
				case 0x98: return 0x98; break;
				case 0x2122: return 0x99; break;
				case 0x459: return 0x9a; break;
				case 0x203a: return 0x9b; break;
				case 0x45a: return 0x9c; break;
				case 0x45c: return 0x9d; break;
				case 0x45b: return 0x9e; break;
				case 0x45f: return 0x9f; break;
				case 0xa0: return 0xa0; break;
				case 0x40e: return 0xa1; break;
				case 0x45e: return 0xa2; break;
				case 0x408: return 0xa3; break;
				case 0xa4: return 0xa4; break;
				case 0x490: return 0xa5; break;
				case 0xa6: return 0xa6; break;
				case 0xa7: return 0xa7; break;
				case 0x401: return 0xa8; break;
				case 0xa9: return 0xa9; break;
				case 0x404: return 0xaa; break;
				case 0xab: return 0xab; break;
				case 0xac: return 0xac; break;
				case 0xad: return 0xad; break;
				case 0xae: return 0xae; break;
				case 0x407: return 0xaf; break;
				case 0xb0: return 0xb0; break;
				case 0xb1: return 0xb1; break;
				case 0x406: return 0xb2; break;
				case 0x456: return 0xb3; break;
				case 0x491: return 0xb4; break;
				case 0xb5: return 0xb5; break;
				case 0xb6: return 0xb6; break;
				case 0xb7: return 0xb7; break;
				case 0x451: return 0xb8; break;
				case 0x2116: return 0xb9; break;
				case 0x454: return 0xba; break;
				case 0xbb: return 0xbb; break;
				case 0x458: return 0xbc; break;
				case 0x405: return 0xbd; break;
				case 0x455: return 0xbe; break;
				case 0x457: return 0xbf; break;
				case 0x410: return 0xc0; break;
				case 0x411: return 0xc1; break;
				case 0x412: return 0xc2; break;
				case 0x413: return 0xc3; break;
				case 0x414: return 0xc4; break;
				case 0x415: return 0xc5; break;
				case 0x416: return 0xc6; break;
				case 0x417: return 0xc7; break;
				case 0x418: return 0xc8; break;
				case 0x419: return 0xc9; break;
				case 0x41a: return 0xca; break;
				case 0x41b: return 0xcb; break;
				case 0x41c: return 0xcc; break;
				case 0x41d: return 0xcd; break;
				case 0x41e: return 0xce; break;
				case 0x41f: return 0xcf; break;
				case 0x420: return 0xd0; break;
				case 0x421: return 0xd1; break;
				case 0x422: return 0xd2; break;
				case 0x423: return 0xd3; break;
				case 0x424: return 0xd4; break;
				case 0x425: return 0xd5; break;
				case 0x426: return 0xd6; break;
				case 0x427: return 0xd7; break;
				case 0x428: return 0xd8; break;
				case 0x429: return 0xd9; break;
				case 0x42a: return 0xda; break;
				case 0x42b: return 0xdb; break;
				case 0x42c: return 0xdc; break;
				case 0x42d: return 0xdd; break;
				case 0x42e: return 0xde; break;
				case 0x42f: return 0xdf; break;
				case 0x430: return 0xe0; break;
				case 0x431: return 0xe1; break;
				case 0x432: return 0xe2; break;
				case 0x433: return 0xe3; break;
				case 0x434: return 0xe4; break;
				case 0x435: return 0xe5; break;
				case 0x436: return 0xe6; break;
				case 0x437: return 0xe7; break;
				case 0x438: return 0xe8; break;
				case 0x439: return 0xe9; break;
				case 0x43a: return 0xea; break;
				case 0x43b: return 0xeb; break;
				case 0x43c: return 0xec; break;
				case 0x43d: return 0xed; break;
				case 0x43e: return 0xee; break;
				case 0x43f: return 0xef; break;
				case 0x440: return 0xf0; break;
				case 0x441: return 0xf1; break;
				case 0x442: return 0xf2; break;
				case 0x443: return 0xf3; break;
				case 0x444: return 0xf4; break;
				case 0x445: return 0xf5; break;
				case 0x446: return 0xf6; break;
				case 0x447: return 0xf7; break;
				case 0x448: return 0xf8; break;
				case 0x449: return 0xf9; break;
				case 0x44a: return 0xfa; break;
				case 0x44b: return 0xfb; break;
				case 0x44c: return 0xfc; break;
				case 0x44d: return 0xfd; break;
				case 0x44e: return 0xfe; break;
				case 0x44f: return 0xff; break;
 
        default: return this.charCodeAt(i);
    }
}
 
/*
	Needed for writing to binary files.
 	See base64.decode method in emmet-app.js
*/
String.fromAsciiCode = function() {
    // reverse for asciiCodeAt() method
    var s = '', i, c = arguments.length;
 
    for(i = 0; i < c; i++)
    	switch(arguments[i]) {
				case 0x80: s += '\u0402'; break;
				case 0x81: s += '\u0403'; break;
				case 0x82: s += '\u201A'; break;
				case 0x83: s += '\u0453'; break;
				case 0x84: s += '\u201E'; break;
				case 0x85: s += '\u2026'; break;
				case 0x86: s += '\u2020'; break;
				case 0x87: s += '\u2021'; break;
				case 0x88: s += '\u20AC'; break;
				case 0x89: s += '\u2030'; break;
				case 0x8a: s += '\u0409'; break;
				case 0x8b: s += '\u2039'; break;
				case 0x8c: s += '\u040A'; break;
				case 0x8d: s += '\u040C'; break;
				case 0x8e: s += '\u040B'; break;
				case 0x8f: s += '\u040F'; break;
				case 0x90: s += '\u0452'; break;
				case 0x91: s += '\u2018'; break;
				case 0x92: s += '\u2019'; break;
				case 0x93: s += '\u201C'; break;
				case 0x94: s += '\u201D'; break;
				case 0x95: s += '\u2022'; break;
				case 0x96: s += '\u2013'; break;
				case 0x97: s += '\u2014'; break;
				//case 0x98: s += '\u0098'; break;
				case 0x99: s += '\u2122'; break;
				case 0x9a: s += '\u0459'; break;
				case 0x9b: s += '\u203A'; break;
				case 0x9c: s += '\u045A'; break;
				case 0x9d: s += '\u045C'; break;
				case 0x9e: s += '\u045B'; break;
				case 0x9f: s += '\u045F'; break;
				//case 0xa0: s += '\u00A0'; break;
				case 0xa1: s += '\u040E'; break;
				case 0xa2: s += '\u045E'; break;
				case 0xa3: s += '\u0408'; break;
				//case 0xa4: s += '\u00A4'; break;
				case 0xa5: s += '\u0490'; break;
				//case 0xa6: s += '\u00A6'; break;
				//case 0xa7: s += '\u00A7'; break;
				case 0xa8: s += '\u0401'; break;
				//case 0xa9: s += '\u00A9'; break;
				case 0xaa: s += '\u0404'; break;
				//case 0xab: s += '\u00AB'; break;
				//case 0xac: s += '\u00AC'; break;
				//case 0xad: s += '\u00AD'; break;
				//case 0xae: s += '\u00AE'; break;
				case 0xaf: s += '\u0407'; break;
				//case 0xb0: s += '\u00B0'; break;
				//case 0xb1: s += '\u00B1'; break;
				case 0xb2: s += '\u0406'; break;
				case 0xb3: s += '\u0456'; break;
				case 0xb4: s += '\u0491'; break;
				//case 0xb5: s += '\u00B5'; break;
				//case 0xb6: s += '\u00B6'; break;
				//case 0xb7: s += '\u00B7'; break;
				case 0xb8: s += '\u0451'; break;
				case 0xb9: s += '\u2116'; break;
				case 0xba: s += '\u0454'; break;
				//case 0xbb: s += '\u00BB'; break;
				case 0xbc: s += '\u0458'; break;
				case 0xbd: s += '\u0405'; break;
				case 0xbe: s += '\u0455'; break;
				case 0xbf: s += '\u0457'; break;
				case 0xc0: s += '\u0410'; break;
				case 0xc1: s += '\u0411'; break;
				case 0xc2: s += '\u0412'; break;
				case 0xc3: s += '\u0413'; break;
				case 0xc4: s += '\u0414'; break;
				case 0xc5: s += '\u0415'; break;
				case 0xc6: s += '\u0416'; break;
				case 0xc7: s += '\u0417'; break;
				case 0xc8: s += '\u0418'; break;
				case 0xc9: s += '\u0419'; break;
				case 0xca: s += '\u041A'; break;
				case 0xcb: s += '\u041B'; break;
				case 0xcc: s += '\u041C'; break;
				case 0xcd: s += '\u041D'; break;
				case 0xce: s += '\u041E'; break;
				case 0xcf: s += '\u041F'; break;
				case 0xd0: s += '\u0420'; break;
				case 0xd1: s += '\u0421'; break;
				case 0xd2: s += '\u0422'; break;
				case 0xd3: s += '\u0423'; break;
				case 0xd4: s += '\u0424'; break;
				case 0xd5: s += '\u0425'; break;
				case 0xd6: s += '\u0426'; break;
				case 0xd7: s += '\u0427'; break;
				case 0xd8: s += '\u0428'; break;
				case 0xd9: s += '\u0429'; break;
				case 0xda: s += '\u042A'; break;
				case 0xdb: s += '\u042B'; break;
				case 0xdc: s += '\u042C'; break;
				case 0xdd: s += '\u042D'; break;
				case 0xde: s += '\u042E'; break;
				case 0xdf: s += '\u042F'; break;
				case 0xe0: s += '\u0430'; break;
				case 0xe1: s += '\u0431'; break;
				case 0xe2: s += '\u0432'; break;
				case 0xe3: s += '\u0433'; break;
				case 0xe4: s += '\u0434'; break;
				case 0xe5: s += '\u0435'; break;
				case 0xe6: s += '\u0436'; break;
				case 0xe7: s += '\u0437'; break;
				case 0xe8: s += '\u0438'; break;
				case 0xe9: s += '\u0439'; break;
				case 0xea: s += '\u043A'; break;
				case 0xeb: s += '\u043B'; break;
				case 0xec: s += '\u043C'; break;
				case 0xed: s += '\u043D'; break;
				case 0xee: s += '\u043E'; break;
				case 0xef: s += '\u043F'; break;
				case 0xf0: s += '\u0440'; break;
				case 0xf1: s += '\u0441'; break;
				case 0xf2: s += '\u0442'; break;
				case 0xf3: s += '\u0443'; break;
				case 0xf4: s += '\u0444'; break;
				case 0xf5: s += '\u0445'; break;
				case 0xf6: s += '\u0446'; break;
				case 0xf7: s += '\u0447'; break;
				case 0xf8: s += '\u0448'; break;
				case 0xf9: s += '\u0449'; break;
				case 0xfa: s += '\u044A'; break;
				case 0xfb: s += '\u044B'; break;
				case 0xfc: s += '\u044C'; break;
				case 0xfd: s += '\u044D'; break;
				case 0xfe: s += '\u044E'; break;
				case 0xff: s += '\u044F'; break;
 
        default: s += String.fromCharCode(arguments[i]);
    }
    return s;
}

Первую табличку мы используем при чтении из бинарного файла средствами JavaScript, вторую – при записи в бинарник.

Например, используем эти таблички для base64 кодирования/декодирования бинарных данных (методы из emmet-app.js)

/**
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @link http://chikuyonok.ru
 */
emmet.define('base64', function(require, _) {
	var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
	return {
		/**
		 * Encodes data using base64 algorithm
		 * @author Tyler Akins (http://rumkin.com)
		 * @param {String} input
		 * @returns {String}
		 * @memberOf emmet.base64
		 */
		encode : function(input) {
			var output = [];
			var chr1, chr2, chr3, enc1, enc2, enc3, enc4, cdp1, cdp2, cdp3;
			var i = 0, il = input.length, b64 = chars;
 
			while (i < il) {
 
				// call .asciiCodeAt() instead of .charCodeAt()
				// see AkelEmmet.js in Scripts folder for details
				cdp1 = input.asciiCodeAt(i++);
				cdp2 = input.asciiCodeAt(i++);
				cdp3 = input.asciiCodeAt(i++);
 
				chr1 = cdp1 & 0xff;
				chr2 = cdp2 & 0xff;
				chr3 = cdp3 & 0xff;
 
				enc1 = chr1 >> 2;
				enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
				enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
				enc4 = chr3 & 63;
 
				if (isNaN(cdp2)) {
					enc3 = enc4 = 64;
				} else if (isNaN(cdp3)) {
					enc4 = 64;
				}
 
				output.push(b64.charAt(enc1) + b64.charAt(enc2) + b64.charAt(enc3) + b64.charAt(enc4));
			}
 
			return output.join('');
		},
 
		/**
		 * Decodes string using MIME base64 algorithm
		 * 
		 * @author Tyler Akins (http://rumkin.com)
		 * @param {String} data
		 * @return {String}
		 */
		decode : function(data) {
			var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, tmpArr = [];
			var b64 = chars, il = data.length;
 
			if (!data) {
				return data;
			}
 
			data += '';
 
			do { // unpack four hexets into three octets using index points in b64
				h1 = b64.indexOf(data.charAt(i++));
				h2 = b64.indexOf(data.charAt(i++));
				h3 = b64.indexOf(data.charAt(i++));
				h4 = b64.indexOf(data.charAt(i++));
 
				bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
 
				o1 = bits >> 16 & 0xff;
				o2 = bits >> 8 & 0xff;
				o3 = bits & 0xff;
 
				if (h3 == 64) {
					tmpArr[ac++] = String.fromAsciiCode(o1); // String.fromCharCode(o1);
				} else if (h4 == 64) {
					tmpArr[ac++] = String.fromAsciiCode(o1, o2); //String.fromCharCode(o1, o2);
				} else {
					tmpArr[ac++] = String.fromAsciiCode(o1, o2, o3); //String.fromCharCode(o1, o2, o3);
				}
			} while (i < il);
 
			return tmpArr.join('');
		}
	};
});

Крепкого Вам чая в бокале, сладкого лимона и главное конечно, я знаю, что банален, но желаю хорошего настроения. От чего оно зависит?

nginx+php-fpm под Windows

Здравствуйте!

nginx

Apache – король веб-серверов, если можно так сказать. Но на пятки ему наступает даже не IIS от Microsoft, не lighttpd, а nginx (произносится как Энджин-Икс, engine с английского мотор, двигатель) нашего соотечественника Сысоева.

Чем он хорош? Говорят, что статика отдаётся гораздо быстрее, чем у Апача, да и динамика я думаю тоже. Он жрёт меньше ресурсов, что может быть критически важно для нагруженных серверов. Раньше мнгоие применял связку nginx+Apache – nginx для отдачи статики (рисунков, js/css etc.), а Апач – для отдачи динамики (PHP/Perl/Python/Ruby etc.). Но теперь nginx можно применять без Апача, так как для него появилось куча плагинов и дополнений, поэтому вместо связки nginx+Apache+PHP (мы тут говорим о PHP-среде) легко настроить просто nginx+php-fpm. Ладно, об нём написано куча литературы, не буду повторяться, опишу лишь процесс установки nginx+php-fpm под Виндовс (Windows).

Хотя, конечно, nginx органичней всего чувствует себя в FreeBSD и Linux (любой Unix-среде, наверное), под Винду он тоже неплохо работает, по крайней мере я его у себя на домашнем компе установил, чтобы тестировать некоторые штуки.

Итак, процесс установки/первичной настройки. Этот процесс расписан здесь: http://nginx.org/ru/docs/windows.html
я приведу лишь выжимку.

Смотрим доступные версии nginx под windows здесь: http://nginx.org/en/download.html
Сейчас есть версия 1.8.0, несколько месяцев назад я устанавливал 1.6.2, которая и сейчас у меня работает.
Итак, скачиваем текущую версию под windows: http://nginx.org/download/nginx-1.8.0.zip

Для удобства примем то, что я пользую сейчас:
Создаём папку C:\usr. Заходим в неё и распаковываем nginx-1.8.0.zip здесь (это можно проделать через GUI-интерфэйс).
Затем запускам териминал и заходим:

> C:
> cd C:\usr\nginx-1.8.0
> start nginx

Тут Виньда может выкинуть окошко с предупреждением (см. скриншот), что nginx пытается получить доступ в сеть. Мы конечно же разрешаем.

allow nginx to network

Проверяем, запущен ли nginx и видим результат:

>tasklist /fi "imagename eq nginx.exe"
 
Image Name                     PID Session Name        Session#    Mem Usage
========================= ======== ================ =========== ============
nginx.exe                     1336 Console                    1      6,440 K
nginx.exe                     3136 Console                    1      6,268 K
nginx.exe                     4864 Console                    1      6,496 K
nginx.exe                     6544 Console                    1      5,872 K

Остановим nginx нормально: nginx -s quit. Есть ещё несколько полезных команд для nginx:
nginx -s stop – останов nginx в любом случае (применяется, если nginx -s quit не сработает).
nginx -s reload – перезагрузка .conf файлов (конфигурации)
nginx -s reopen – переоткрытие .log файлов (полезна, если мы удалили или переместили логи при работающем nginx).

Итак, мы остановили nginx сейчас, так как прежде чем его запускать, надо правильно настроить .conf файлы. Они расположены в папке conf. Стандартный файл настройки – nginx.conf, из него директивой include могут подсоединяться другие файлы из этой (впрочем, и из любой другой) папки.
Например, директива include mime.types; в секции http присоединит файл mime.types, в котором находится определения всех стандартных MIME-типов. Впрочем, сам конфиг я обсуждать здесь не буду, о нем много написано в инете, приведу лишь пример своего конфига с краткими пояснениями.

Предупреждение: это конфиг для моей домашней тестовой среды. Для рабочего сервера требуется более тонкая настройка.

worker_processes  1;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;
    
    #
    # Формат лога делаем как у Апача
    #
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  logs/access.log  main;

    # sendfile        on;
    #tcp_nopush     on;
  
    keepalive_timeout  65;

    # Сжатие gzip на лету
    gzip  on;

    server {
        # listen       801;
        server_name  localhost;
				autoindex on; # allow dir listing
				root E:/sites;
				
        #charset koi8-r;
        #access_log  logs/host.access.log  main;

	#
        # запретим доступ ко всем файлам, начинающимся с точки 
	#
        location ~ /\. {
            deny  all;
        }

        location / {
            root   E:/sites;
            index  index.html index.htm index.php;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

	#
        # передаем все PHP скрипты серверу FastCGI, слушающему на 127.0.0.1:9123
        #
        location ~ \.php$ {
            root           E:/sites;
            fastcgi_pass   127.0.0.1:9123;
            fastcgi_index  index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }
    }
}

Итак, в этом конфиге большинство настроек оставлено по умолчанию, а корень сайтов у нас в E:\sites, что в первую очередь делает команда root E:/sites. Обратите внимание на прямые слэши в стиле Unix в пути к папкам и файлам – это требование nginx, даже для Windows-версии.

Теперь можно запускать nginx (start nginx), если мы его останавливали перед этим, либо применить команду nginx -s reload, чтобы сервер перечитал конфиги без остановки своей работы, что полезно при работающем внешнем сайте.

Итак, теперь надо настроить PHP-FPM для Windows. Учтите, что мы уже в нашем конфиге сделали его поддержку на порту 9123 (под-секция location ~ \.php$)

PHP-FPM для Windows

1. Скачиваем свежий (или версию по выбору) .zip-архивчик с http://windows.php.net/download/. Архивчик должен быть VC11/VC9, что содержит в себе FastCGI-файл (phpcgi.exe).
2. Создаем папку в C:\usr, например с именем php-5.6.9 и распаковываем в неё содержимое архива.
3. Редактируем файл php.ini в соотв. со своими предпочтениями, единственное, убедиться, что у нас есть такая строка:

# nginx security setting
cgi.fix_pathinfo=0

Она закрывает одну из старых уязвимостей nginx. Далее можно подключить PHP-модули по вкусу, расскоментировав их в соотв. секции и произвести другие настройки.

4. Теперь создадим .bat-файл, например php-fpm-start.bat с таким содержимым:

@echo off
echo Starting PHP FastCGI...
set path=c:\usr\php-5.6.9;%PATH%
C:\usr\php-5.6.9\php-cgi.exe -b 127.0.0.1:9123 -c C:\usr\php-5.6.9\php.ini

и запустим его. Если мы его запускаем из GUI, то появится окно консоли и останется открытым, придётся с этим смириться.

Всё, наш сервер мы уже давно настроили на соединение с этим PHP процессом.

Для проверки создаём файл index.php в папке E:\sites с таким содержимым:

<?php
phpinfo();

Теперь направляем наш любимый браузер на http://localhost и видим такую примерно картину:

phpinfo() начало

phpinfo

phpinfo() с версией nginx

phpinfo nginx

Здесь же можно посмортеь переменные среды и подключаемые модули. Для совместимости nginx создет переменные среды, совместимые с апачевскими, например _SERVER[“SERVER_NAME”], _SERVER[“DOCUMENT_ROOT”], _SERVER[“REQUEST_URI”], _SERVER[“SCRIPT_NAME”] и т.д., и мы можем использовать их в своих PHP-сценариях, как делали это в случае с Апачем.

До свидания!

PHP: HTML::render – грамотно рендерим свои странички

Здравствуйте.

Лет 5 назад мы работали над одним сайтом, и использовали какой-то open source framework, я уже не помню какой именно. Так вот, он мне нравился, хоть и был слегка замороченный, я в нём ковырялся, и наткнулся на функцию рендеринга конечной веб-страницы. Она мне показалась довольно милой, я её максимально сократил, оставив практически всю функциональность, и облёк в форму класса.

Это мощный рендерер занимает всего строчек 40, очень удобный и быстрый. Я им пользуюсь уже 5 лет и ни разу не пожалел об этом. Всегда используйте этот рендерер, даже в демо-проектах, нечего сваливать PHP-код и HTML-разметку в кучу.

HTML::render

Вот его полный код:

///////////////////////////////////////////////
//////////// PARSE HTML FUNCTIONS ////////////
/////////////////////////////////////////////
// use STATIC rendering - используем статический рендеринг
class HTML
{
        // deafult folder for html pages (templates)
	static private $folder = 'html';
	// change default folder for html templates
	static public function changeFolder($folder) {
		self::$folder = $folder;
	}
 
	static public function render($template, $data = array()) {
		$content = file_get_contents(self::$folder."/{$template}.html");
		$content = self::design_render_text($content, $data);
		return $content;
	}
 
	static private function design_render_text($content, $data = array()) {
		$content = self::design_parse_function($content, $data);
		$content = self::design_parse($content, $data);
		return $content;
	}
	static private function design_parse_function($content, $data = array()) {
		preg_match_all('/\<\<(.*?)\>\>/is', $content, $res);
		if (@$res[1])
			foreach ($res[1] as $el) {
				$middle = self::design_parse($el, $data);
				$middle = '$result = '.$middle.';';
				eval($middle);
 
				$content = str_ireplace('<<'.$el.'>>', $result, $content);
			}
		return $content;
	}
	static private function design_parse($content, $data) {
		preg_match_all('/\%\%(.*?)\%\%/si', $content, $res);
		if (@$res[1])
			foreach ($res[1] as $el) $content = str_ireplace('%%'.$el.'%%', $data[$el], $content);
		return $content;
	}
}

Достаточно включить этот класс в свой проект и вы можете легко и просто рендерить странички. Как он работает?

Синтаксис:
HTML::render( template [ , array ] );

template – имя шаблона веб-странички. Используется без расширения. По умолчанию шаблоны расположены в папке html (её можно поменять вызовом HTML::changeFolder(‘tpl’), чтобы использовать папку tpl вместо html, к примеру). Шаблоны в папке должны иметь расширение html. Можно использовать любое кол-во вложенных подпапок, естественно.

array – ассоциативный массив, все ключи которого сопоставляются с вхождениями %%…%% в шаблоне и с заменой их значениями. Можно не использовать, если на страничке нет таких элементов.

Например:

$html = array('TITLE' => 'Добро пожаловать!');
$page = HTML::render('index', $html);
die($page);

и браузер выдаст нам отрендеренную страничку. Если в шаблоне html/index.html будет присутствовать вхождение %%TITLE%%, оно будет заменено в данном случае на Добро пожаловать!

Странички состоят из простейших шаблонов – HTML-код + вставка переменных и любых PHP-конструкций.
%%VAR%% – используем подстановку для VAR. VAR – ключ ассоциативного массива, который идёт вторым параметром в вызове HTML::render() (см. выше).
<<PHP_code>> – выполнение PHP кода и отображение на страничке его результата. Можно использовать любые доступные при вызове рендерера функции, и супер-глобальные переменные, например <<$_SERVER[‘HTTP_HOST’]>>
Также может быть использовано для логического ветвления в шаблоне, когда рендерер отобразит особую часть из шаблона при выполнении определённого условия. Пример смотрите ниже.

Пример

Но всё слова-слова, но без примера не всё так понятно, верно.

Вот наш шаблон:

<!DOCTYPE html>
<html lang="ru">
 
<head>
	<meta charset="utf-8">
	<title>Ошибка соединения с БД</title>
</head>
 
<body>
<h1 style="color: blue">Ошибка соединения с БД.<br>
Повторите попытку через минуту!</h1>
<<('%%DEBUG_MODE%%')?'
<div class="info">
%%DB_ERR%%
</div>':''>>
</body>
</html>

Использование:

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';
 
try {
    $dbh = new PDO($dsn, $user, $password);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die(HTML::render('err/db', 
      array('DEBUG_MODE' => $_DEBUGGING, 'DB_ERR' => $e->getMessage())))
}

В этом примере делается попытка соединиться с БД, и если она не удалась, выводится страница из шаблона, находящегося в файле html/err/db.html. %%DB_ERR%% заменяется значением $e->getMessage(), и оно будет включено в страничку, только если %%DEBUG_MODE%%, который преобразуется в true/false согласно переменной $_DEBUGGING, будет равен true.

Этот класс можно применять не только для рендеринга цельных (оконечных) страничек, но и для обработки любых HTML-кусков. Да, возможно не хватает выполнения циклических блоков, как в Smarty, например, но для 40-ка строчек PHP-кода, я полагаю, и это совсем неплохо. Любую нужную функциональность уже добавляем по мере необходимости и по прихоти клиентов.

Удачи на даче! И крепкого чая, крепкой любви и крепкого имбиря.