воскресенье, 30 сентября 2012 г.

Хранение сессий в MySQL в Perl-фреймворке Dancer

Для хранения сессий Dancer в базах данных существует модуль Dancer::Session::DBI. В настоящее время он поддерживает хранение сессий лишь в MySQL и SQLite. В Debian этот модуль отсутствует, поэтому поставим его с помощью dh-make-perl:
# dh-make-perl --install --cpan Dancer::Session::DBI
Также этому модулю понадобится пакет libjson-perl (фактически это модуль JSON для Perl), для того, чтобы сохранять переменные сессии в поле таблицы MySQL.
# apt-get install libjson-perl
Теперь создадим в базе данных таблицу для хранения сессий. Судя по документации, у неё должно быть два обязательных поля: id - из 40 символов и session_data - текстовое поле, в которое и будут сохранятся переменные сессии. Также полезно добавить поле last_active, которое будет содержать дату и время последнего обновления записи. Это поле можно использовать для периодического удаления устаревших сессий. Создадим таблицу:
mysql >
CREATE TABLE `session` (
  `id` char(40) NOT NULL,
  `session_data` text,
  `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Осталось настроить наше приложение на использование установленного модуля сессий и можно начинать им пользоваться. Настроим приложение, добавив следующие строчки в файл config.yml в каталоге проекта:
session: "DBI"
session_options:
  dsn:      "DBI:mysql:database=base;host=localhost;port=3306"
  table:    "session"
  user:     "session"
  password: "session"

serializer: "JSON"
Теперь создадим шаблон страницы, который будет использоваться для запроса имени пользователя и пароля для аутентификации. Шаблон назовём login.tt и положим в каталог views проекта:
<div align="center">
  <form method="POST">
    <table>
      <tr>
        <td>
          <label for="login">Логин:</label>
        </td>
        <td>
          <input type="text" id="login" name="login">
        </td>
      </tr>
      <tr>
        <td>
          <label for="password">Пароль:</label>
        </td>
        <td>
          <input type="password" id="password" name="password">
        </td>
      </tr>
      <tr>
        <td colspan="2" style="text-align: center;">
          <input type="submit" name="ok" value="Войти">
        </td>
      </tr>
    </table>
  </form>
</div>
И создадим шаблон страницы, которая будет показываться только пользователям, прошедшим аутентификацию:
<h3 align="center"><TMPL_VAR NAME="name"></h3>
<div align="center"><a href="/logout">Выйти</a></div>
Теперь опишем поведение приложения, которое будет показывать эти страницы и обрабатывать ввод пользователя. Откроем файл test.pm в каталоге lib и добавим туда обработчики GET- и POST-запросов:
get '/login' => sub {
  template 'login';
};

post '/login' => sub {
  my $login = param "login";
  my $password = param "password";

  if (($login eq "stupin") && ($password eq "test"))
  {
    session user_id => 1;
    session name => "Владимир Ступин";
  }
  redirect '/restricted';
};

get '/logout' => sub {
  session->destroy();
  redirect '/login';
};

get '/restricted' => sub {
  if (not session('user_id')) {
    redirect '/login';
  }
  template 'restricted', { name => session('name') };
};
Здесь есть два обработчика страницы по адресу /login - первый просто выводит шаблон страницы аутентификации, второй - проверяет введённые логин и пароль, создаёт новую сессию и переадресует посетителя на страницу с ограниченным доступом /restricted, если логин и пароль введены правильно.

Обработчик страницы по адресу /logout. При попадании на неё сессия завершается, а посетитель перенаправляется на страницу /login.

Обработчик страницы по адресу /restricted проверяет, что имеется активный сеанс, в котором есть переменная с именем user_id. Если такой переменной или сессии нет, пользователь переадресуется на страницу входа /login. Если всё в порядке, то пользователю показывается страница с информацией для аутентифицированных пользователей.

К сожалению, не всё с модулем Dancer::Session::DBI оказалось так гладко. Пришлось немного доработать его напильником, чтобы русские буквы сохранялись в базе данных в правильной кодировке и чтобы он не выдавал ошибку, если пользователь приходит с идентификатором сессии, которой нет в базе.

Первая доработка в файле /usr/share/perl5/Dancer/Session/DBI.pm выглядит следующим образом:
sub _dbh {
    my $self = shift;
    my $settings = setting('session_options');

    # Prefer an active DBH over a DSN.
    return $settings->{dbh}->() if defined $settings->{dbh};

    # Check the validity of the DSN if we don't have a handle
    my $valid_dsn = DBI->parse_dsn($settings->{dsn} || '');

    die "No valid DSN specified" if !$valid_dsn;

    if (!defined $settings->{user} || !defined $settings->{password}) {
        die "No user or password specified";
    }

    # If all the details check out, return a fresh connection
    my $dbh = DBI->connect($settings->{dsn}, $settings->{user}, $settings->{password});
    if ((defined $dbh) && (defined $settings->{charset}))
    {
      my $sth = $dbh->prepare("SET CHARACTER SET ?");
      $sth->execute($settings->{charset});
      $sth->finish();

      $dbh->{mysql_enable_utf8} = 1 if $settings->{charset} eq "UTF8";
    }
    return $dbh;
}
Вторая доработка в этом же файле выглядит следующим образом:
sub retrieve {
    my ($self, $session_id) = @_;

    my $session = try {
        my $quoted_table = $self->_quote_table;

        my $sth = $self->_dbh->prepare_cached(qq{
            SELECT session_data
            FROM $quoted_table
            WHERE id = ?
        });

        $sth->execute( $session_id );
        my ($session) = $sth->fetchrow_array();
        $sth->finish();

        $session = "{}" unless defined $session;
        $self->_deserialize($session);
    } catch {
        warning("Could not retrieve session ID $session_id - $_");
        return;
    };

    return bless $session, __PACKAGE__ if $session;
}
Первая доработка позволяет указать кодировку клиента, которую клиент должен установить сразу после подключения к базе данных. Настроим кодировку в файле настройки проекта config.yml, вместе с ней настройки модуля сессий примут следующий вид:
session: "DBI"
session_options:
  dsn:      "DBI:mysql:database=base;host=localhost;port=3306"
  table:    "session"
  user:     "session"
  password: "session"
  charset:  "UTF8"
Само собой, это приложение лишь демонстрирует использование сессий в Dancer. В реальном приложении нужно написать функции аутентификации, использующие таблицу пользователей и функции, ограничивающие доступ к страницам и операциям, использующие таблицы групп пользователей и их прав. Напоследок, пара снимков экрана с двумя страницами:

воскресенье, 23 сентября 2012 г.

Настройка nginx

Решил попробовать веб-сервер nginx вместо более привычного мне lighttpd. Разница между ними, на мой взгляд, как между правым и левым ботинком, но тем не менее, первый, судя по проникающему в мои уши информационному шуму, почему-то более популярен.

Поставим нужные нам пакеты nginx и spawn-fcgi:
# apt-get install nginx spawn-fcgi
Главный файл конфигурации - /etc/nginx/nginx.conf. В нём много комментариев, но выжимку из него получить можно с помощью простой команды grep -vE "^\s*$|^\s*#" /etc/nginx/nginx.conf. После моей небольшой доработки, в которой я снизил количество процессов с 4 до 1, файл принял следующий вид:
user www-data;
worker_processes 1;
pid /var/run/nginx.pid;
events {
  worker_connections 768;
}
http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}
Система, на которой я настраиваю этот веб-сервер - домашняя и не предполагает высокой нагрузки. Больше в этом конфиге я ничего не менял.

Следующий интересующий меня файл - это, как ни странно, сценарий инициализации процесса spawn-fcgi, родственного веб-серверу lighttpd, который будет обслуживать php-страницы. Его я взял из своей заметки Шпаргалка по настройке веб-сервера Lighttpd и доработал под мой случай. Поскольку веб-сервер будет работать с бэкэндом на одном компьютере, для общения между ними выгоднее использовать Unix-сокет, а не TCP-соединение. Также зададим 4 процесса-бэкэнда, так что одновременно может исполняться до 4 скриптов, формирующих страницы, запрошенные клиентами:
#!/bin/sh

### BEGIN INIT INFO
# Provides:          spawn-fcgi-php
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts FastCGI for PHP
# Description:       starts FastCGI for PHP using start-stop-daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
NAME=spawn-fcgi-php
PID=/var/run/spawn-fcgi-php.pid
DAEMON=/usr/bin/spawn-fcgi
#DAEMON_OPTS="-f /usr/bin/php-cgi -a 127.0.0.1 -p 8080 -u www-data -g www-data -P $PID"
DAEMON_OPTS="-f /usr/bin/php-cgi -s /var/run/php.sock -F 4 -u www-data -g www-data -P $PID"

test -x $DAEMON || exit 0

set -e

case "$1" in
  start)
    echo "Starting $NAME: "
    start-stop-daemon --start --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
    echo "done."
    ;;
  stop)
    echo "Stopping $NAME: "
    start-stop-daemon --stop  --pidfile $PID --retry 5
    rm -f $PID
    echo "done."
    ;;
  restart)
    echo "Stopping $NAME: "
    start-stop-daemon --stop  --pidfile $PID --retry 5
    rm -f $PID
    echo "done..."
    sleep 1
    echo "Starting $NAME: "
    start-stop-daemon --start --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
    echo "done."
    ;;
  *)
    echo "Usage: /etc/init.d/$NAME {start|stop|restart}" >&2
    exit 1
    ;;
esac

exit 0
Теперь добавим этот скрипт в автозагрузку и приступим к самому интересному - к собственно настройке веб-сервера:
# chmod +x /etc/init.d/spawn-fcgi-php
# update-rc.d spawn-fcgi-php defaults
Создадим в каталоге /etc/nginx/sites-available/ файл mysite со следующим содержимым:
server {
  root /var/www/;
  index index.php index.html index.htm;
  server_name localhost;
  location /osm/ {
    proxy_pass http://127.0.0.1:8080/osm/ ;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
  location / {
    try_files $uri $uri/ / ;
  }
  location /doc/ {
    alias /usr/share/doc/;
    autoindex on;
    allow 127.0.0.1;
    allow ::1;
    deny all;
  }
  location /postadmin/ {
    auth_basic "postadmin";
    auth_basic_user_file /etc/nginx/htpasswd;
  }
  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php.sock;
    fastcgi_index index.php;
    include fastcgi_params;
  }
}
Этот файл показывает сразу несколько интересных (по крайней мере мне) примеров настройки. Из него видно, как создать псевдоним каталога, ограничить доступ по IP-адресам, ограничить доступ по паролю, обслуживать часть сайта, перенаправляя запросы другому веб-серверу, и исполнять php-скрипты, направляя запросы ранее настроенному процессу spawn-fcgi-php (аналогично spawn-fcgi можно использовать для управления другими fastcgi-процессами, например, написанными на Perl).

Переключим nginx на использование созданного файла конфигурации:
# unlink /etc/nginx/sites-enabled/default
# ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite
Думаю, что настройки не нуждаются в особых пояснениях. Часть из них понятна без комментариев, а часть является чёрной магией, специфичной для nginx.

Создать файл паролей можно с помощью утилиты htpasswd из пакета apache2-utils:
$ htpasswd -c /etc/nginx/htpasswd stupin
И запустим веб-сервер:
# /etc/init.d/nginx start
По итогам настройки могу сделать следующие неутешительные для nginx выводы:
1. Он не умеет самостоятельно порождать и прибивать локальные fastcgi-процессы, как это умеет делать lighttpd,
2. Он не поддерживает digest-авторизацию, что называется "из коробки". Существует модуль, поддерживающий digest-авторизацию, но он распространяется отдельно от nginx и не пакетирован в Debian.
3. Он не имеет родственного пакета, управляющего fastcgi-процессами, поэтому необходимый spawn-fcgi приходится брать из отдельного пакета, который изначально делался вместе с веб-сервером lighttpd.

Не то что бы это плохой веб-сервер, но мне он показался немного сыроват по сравнению с используемым мной lighttpd, которым я пользуюсь уже почти 5 лет. В плюс nginx'у можно записать лёгкость настройки.

Интересно будет услышать мнение людей, использующих nginx, которым по каким-то разумным причинам не подошёл lighttpd.

воскресенье, 16 сентября 2012 г.

Шаблоны HTML::Template в Perl-фреймворке Dancer

Захотелось немного изучить веб-фреймворк Dancer, т.к. писать с использованием модуля CGI и его компании хоть и можно, но не удобно. Особенно раздражает неудобство в использовании cookie, а ещё больше - неудобство установки или удаления cookie в редиректе. Хотя, конечно, этим всё не ограничивается.

Ставим фреймворк:
# apt-get install libdancer-perl
Создаём каркас будущего приложения под названием test:
$ dancer -a test
Теперь можно перейти в каталог test/bin и запустить оттуда приложение app.pl:
$ cd test/bin
$ ./app.pl
Запустится только что созданное приложение test, которое будет ожидать подключений на TCP-порту 3000. Можно открыть в браузере ссылку http://localhost:3000 и увидеть тестовую страницу:

Как вы уже догадались по заголовку заметки, необычное в этом снимке экрана только одно - строчка "Template engine: html_template".

По умолчанию в Dancer'е используется некий "простой" шаблонизатор simple. В качестве альтернативы предлагается использовать template_toolkit, я же хочу воспользоваться html_template, который по сегодняшним временам кому-то может показаться довольно "дубовым".

К сожалению, в репозитории Debian этого модуля нет. Поэтому поставим сначала пакет dh-make-perl:
# apt-get install dh-make-perl
Теперь с его помощью поставим нужный нам Perl-модуль Dancer::Template::HtmlTemplate, соглашаясь при первом запуске на все умолчальные настройки:
# dh-make-perl --install --cpan Dancer::Template::HtmlTemplate
Теперь укажем Dancer'у, что мы хотим использовать этот шаблонизатор в нашем приложении. Для этого закомментируем в файле настроек config.yml в каталоге проекта строчку шаблонизатора simple и добавим строчку с шаблонизатором html_template:
#template: "simple"
template: "html_template"
Теперь самое время отредактировать шаблоны в соответствии с правилами выбранного нами шаблонизатора - заменим все строчки типа "<% var %>" на строчки типа "<TMPL_VAR NAME="var">". Шаблоны находятся в каталоге views проекта и имена их файлов оканчиваются расширением ".tt".

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

Если заглянуть в исходный текст модуля, то можно увидеть, что опции HTML::Template, используемые при загрузке шаблона, можно настроить всё в том же файле настройки config.yml:
my $ht = HTML::Template->new(
  filename => $template,
  die_on_bad_params => 0, # Required, as we pass through other params too
  %{$self->config}
);
Я использую настройки utf8 и loop_context_vars. Их я и пропишу в файл config.yml:
engines:
  html_template:
    utf8:              1
    loop_context_vars: 1
Теперь создадим в каталоге views шаблон страницы hello, поместив в файл hello.tt следующий текст:
<h3 align="center">
<TMPL_VAR NAME="name">
</h3>
И откроем файл test.pm в каталоге lib, допишем в него обработчик страницы hello:
get '/hello/:name' => sub {
  template 'hello', { name => param('name') };
};
Теперь перейдём по ссылке "http://localhost:3000/hello/превед" и увидим следующее:

Страница создана на основе макета, лежащего в файле views/layouts/main.tt и созданного нами шаблона view/hello.tt. Если захочется поменять что-то в макете - можно отредактировать его, можно создать новый макет и положить его рядом. Выбрать используемый макет можно прямо в обработчике. В документации приведены два примера.

Отключение макета на определённой странице:
get '/' => sub {
  template 'index', {}, { layout => undef };
};
Или использование указанного макета:
get '/user' => sub {
  template 'user', {}, { layout => 'user' };
};
На этом пока всё.

воскресенье, 9 сентября 2012 г.

Perl и UTF-8

Оставим в стороне теорию, о ней достаточно написано из без меня, например, здесь Пара слов про UTF-8 и здесь UTF Perl Practice, или как использовать UTF-8 в перле. После прочтения теории я занялся практикой и сделал для себя следующие практические заготовки.

Включаем флаг UTF-8 для всех строк в программе:
use utf8;
Выставляем флажок UTF-8 для всех строк, прочитанных или записанных в текстовые файлы, включая стандартные потоки ввода-вывода:
use open qw(:std :utf8);
Странным образом предыдущие настройки включают блочную буферизацию стандартных потоков вывода. Отключаем её:
select(STDERR);
$| = 1;
select(STDOUT);
$| = 1;
Выставляем флажок UTF-8 для всех строк, полученных в переменных CGI:
use CGI qw(:cgi -utf8);
Выставляем флажок UTF-8 для файла, с помощью которого грузится шаблон HTML::Template и все подшаблоны. К сожалению, не нашёл в документации модуля способа однократно задать использование этой настройки глобально.
my $template = HTML::Template->new(filename => "/var/www/index.tmpl",
                                   utf8 => 1,
                                   die_on_bad_params => 0,
                                   global_vars => 1);
Чтобы модуль DBI выставлял флаг UTF-8 для всех строк, прочитанных из БД MySQL или переданных ей, выставляем настройку в дескрипторе соединения.
$dbh->{mysql_enable_utf8} = 1;
Аналогично для PostgreSQL:
$dbh->{pg_enable_utf8} = 1;
Нужно отметить, что эти настройки для DBI не указывает кодировку клиента, а только лишь дают инструкцию модулю выставлять флажок UTF-8 у строк.

На этом, вроде, всё.

воскресенье, 2 сентября 2012 г.

Настройка vtun в Debian

Опишу настройку vtun для довольно экзотической задачи, которая тем не менее даёт хорошее представление о возможностях vtund.

Постановка задачи

Имеется две локальные сети network1 и network2 за шлюзами gateway1 и gateway2 соответственно. Шлюз gateway1 имеет "белый" IP-адрес, а второй шлюз может выходить в интернет только через NAT на вышестоящем шлюзе nat_gateway:
   _____________    ________                    _____________      ________       _____________
  /            /   |        |                  |             |    |        |     /            /
 /  network1  /----|gateway1|----/internet/----O nat_gateway |----|gateway2|----/  network2  /
/____________/     |________|                  |_____________|    |________|   /____________/
Требуется, чтобы сеть network1 могла ходить в сеть network2, но таким образом, чтобы узлы из сети network2 не знали о существовании каких-то других сетей, а считали, что с ними связывается непосредственно сам шлюз gateway2. То есть, необходимо на шлюзе gateway2 настроить трансляцию адресов network1 в локальный адрес шлюза gateway2.

Сеть network1: 10.0.0.1/24

Сеть network2: 192.168.0.0/24

Доменное имя gateway1 в интернете - server.domain.tld.

Настроим vtund на шлюзе gateway1 в режиме сервера

В файл /etc/default/vtund внесём следующую строчку:
RUN_SERVER=yes
В файле /etc/vtund.conf опишем конфигурацию сервера:
options {
  type stand;
  port 5000;

  bindaddr {
    iface eth0;
  };

  ifconfig /sbin/ifconfig;
  ip /sbin/ip;
  firewall /sbin/iptables;
}

vpn1 {
  passwd mysecret;
  type tun;
  proto tcp;
  compress no;
  encrypt yes;
  keepalive yes;
  stat yes;
  multi killold;

  up {
    ifconfig "%% 192.168.1.1 pointopoint 192.168.1.2";
    ip "route add 192.168.0.0/24 dev %%";
  };

  down {
    ifconfig "%% down";
    ip "route delete 192.168.0.0/24 dev %%";
  };
}
Настроим vtund на шлюзе gateway2 в режиме клиента

В файл /etc/default/vtund внесём следующие строчки:
CLIENT0_NAME="vpn1"
CLIENT0_HOST="server.domain.tld"
В файле /etc/vtund.conf опишем конфигурацию клиента:
options {
  type stand;
  port 5000;

  ifconfig /sbin/ifconfig;
  ip /sbin/ip;
  firewall /sbin/iptables;
}

vpn1 {
  passwd mysecret;
  type tun;
  proto tcp;
  compress no;
  encrypt yes;
  stat yes;
  persist yes;

  srcaddr {
    iface eth0;
  };

  up {
    ifconfig "%% 192.168.1.2 pointopoint 192.168.1.1";
    ip "route add 10.0.0.0/24 dev %%";
    firewall "-t nat -A POSTROUTING -o eth0 -j MASQUERADE";
  };

  down {
    ifconfig "%% down";
    ip "route delete 10.0.0.0/24 dev %%";
    firewall "-t nat -D POSTROUTING -o eth0 -j MASQUERADE";
  };
}
Описание работы настроенной системы

Итак, работать это будет следующим образом. Шлюз gateway2 устанавливает соединение со шлюзом gateway1, используя доменное имя сервера server.domain.tld, TCP-порт сервера 5000 и пароль mysecret. Между шлюзами устанавливается туннель точка-точка, через который шлюз gateway1 будет отправлять пакеты для сети network2 192.168.0.0/24, находящийся за шлюзом gateway2. В свою очередь, шлюз gateway2 будет отправлять пакеты для сети network1 10.0.0.0/24 через шлюз gateway1.

При этом, если узел сети network1 10.0.0.0/24 попытается связаться с узлом из сети network2 192.168.0.0/24, шлюз gateway2 будет транслировать адреса сети network1 10.0.0.0/24 в свой локальный адрес (в терминологии iptables это называется SNAT или MASQUERADE).

В случае обрыва соединения gateway2 будет самостоятельно переустанавливать подключение. gateway1, при этом, допускает подключение только одного клиента и все предыдущие подключения будут заменяться вновь установленными.

Особенности, достоинства и недостатки

Файл /etc/default/vtund допускает настройку установки до 10 клиентских соединений (легко увеличивается редактированием скрипта инициализации /etc/init.d/vtun). Количество соединений, принимаемых сервером, не ограничено - их можно настроить в файле /etc/vtund.conf. При этом vtund может быть одновременно клиентом для одной части соединений и сервером для другой части соединений.

Из недостатков можно отметить только один - для каждого соединения необходимо прописывать собственные настройки, т.к. vtund не поддерживает встроенную возможность раздавать клиентам адреса из заранее заданного пула. Теоретически это можно реализовать с помощью внешних скриптов. Я считаю, что это не очень большой недостаток, т.к. этот недостаток является оборотной стороной достоинства vtund - простоты настройки (в отличие, например, от openvpn).

При настройке vtund я воспользовался собственными переводами страниц руководства: vtund.conf(5) и vtund(8)