Общая структура

В основе системы закачки файлов находится база данных (MySQL v5.1). Основное её предназначение – хранение очереди закачек. Помещение новых объектов в эту очередь происходит через интерфейсную часть, в качестве которой я выбрал сервис ICQ. Непосредственно закачку осуществляет скрипт ddd.pl, выбирающий из базы данных информацию об очередном скачиваемом объекте. Вся система закачки базируется на ОС FreeBSD 6.2.

Каким же функционалом будет наделена наша система закачек? Каждый из закачиваемых объектов имеет приоритет (значения от 0 до 255, где 255 – наивысший приоритет), в соответствии с которым демон ddd.pl будет осуществлять выборку из БД очередного объекта скачивания. Я говорю об объектах скачивания, потому как система закачек в моей реализации поддерживает ftp- и http-ссылки и достаточно просто расширяема для работы с другими протоколами. Далее, желательно, чтобы система закачек не занимала весь канал под свои нужны, поэтому необходимо встроить средства ограничения скорости передачи данных (на основе DUMMYNET). 

Закачка файлов будет происходить под пользователем icq_bot (входящего в группу nobody), а скаченные файлы будут размещаться в
/home/icq_bot/allfiles/. Файрвол будет ограничивать скорость передачи данных для всех процессов пользователя
icq_bot.

Рассмотрим реализацию.

 

Конфигурирование ядра

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

options IPFIREWALL # активизировать ipfw
options DUMMYNET # активизировать поддержку пайпов и очередей

Пересоберем ядро и приступим к конфигурированию полученной операционной системы.

 

Конфигурирование FreeBSD

Для начала активизируем запуск скрипта /etc/rc.firewall при загрузке системы. Для этого добавим следующую строку в файл
/etc/rc.conf:

firewall_enable=YES

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

/sbin/ipfw pipe 1 config bw 64Kbit/s
/sbin/ipfw add 5000 pipe 1 ip from any to me uid icq_bot 

В последнем добавленном правиле мы ограничиваем скорость только у процессов, выполняемых под пользователем icq_bot. Создадим в домашней директории пользователя icq_bot директорию
allfiles:

cd /home/icq_bot && mkdir allfiles
chown icq_bot:nobody allfiles

В эту директорию впоследствии будут помещаться все скачанные файлы.

 

База данных 

База данных предназначена для хранения очереди закачек. Создадим отдельную базу данных icq_bot и назначим ей пользователя с правами на запись и чтение. В полученной базе создадим таблицу, описанную ниже.

Имя столбца  Тип данных  Описание
id  SMALLINT(4) UNSIGNED NOT NULL AUTO_INCREMENT  Первичный ключ, однозначно идентифицирующий запись в таблице.
add_time  DATETIME NOT NULL  Время добавления в очередь закачки новой задачи.
type  VARCHAR(10) NOT NULL  Тип скачиваемого ресурса. В зависимости от значения этого поля будет определяться функция (в которой вызывается некая утилита), инициирующая закачку ссылки
link. Т.к. для закачки ftp- и http-файлов используется одна утилита
(wget), то разумно объединить эти типы. Было выбрано название
htftp.
link  TEXT NOT NULL  Ссылка на закачиваемый ресурс
start_time  DATETIME  Время начала закачки (его устанавливает демон
ddd.pl)
end_time  DATETIME  Время окончания закачки (его также устанавливает демон
ddd.pl)
prior  TINYINT UNSIGNED NOT NULL DEFAULT 50  Приоритет закачки – значение от 0 до 255, в соответствии с которым сортируется список задач для скачивания.
 

ICQ интерфейс

Для работы по протоколу ICQ были найдены модули Net::ICQ2000 и
Net::OSCAR. Было решено остановиться на последнем – его программный интерфейс более прост. Помимо этого NET::OSCAR есть в портах:

cd /usr/ports/net-im/p5-Net-OSCAR
make install

Рассмотрим сокращенный вариант кода ICQ-бота
(icq_bot.pl):

#!/usr/bin/perl


use Net::OSCAR;
use DBI;

# конфигурационные данные
$MAX_SPEED=512;
$DB_NAME="icq_bot";
$DB_HOST="localhost";
$DB_USER="user";
$DB_PASS="pass";
#
UIN администратора бота
$ADM_UIN=999999999; 

# хеш, содержащий имена команд и связанные с ними функции
%functions = ("speed" =>\&speed,
"htftp" => \&htftp,
"showlist" => \&showlist,
"del" => \&del,
"chgprior" => \&chgprior);

# подключение к ICQ-серверу
$oscar = Net::OSCAR->new();
#
функция-обработчик входящих сообщений
$oscar->set_callback_im_in(\&message_callback);
#
здесь поместите свой UIN и пароль
$oscar->signon(666666, "passpasspass") or 
die("Авторизация не удалась!\n");
#
бесконечный цикл
$quit = 0;
while(!$quit) { $oscar->do_one_loop(); }

#
#
ядро бота
#
sub message_callback {
local($cmd, $params, $result);
local $result = "";
local($client, $uin, $msg) = @_;
$ADM_UIN=new Net::OSCAR::Screenname $ADM_UIN;
#
разрешаем управление ботом только с UIN`а $ADM_UIN
return if($uin ne $ADM_UIN);
#
получаем имя команды(с префиксом “!”) и её параметры
if(($cmd, $params) = $msg =~ /^\!([^\ ]+)\s*(.*)$/)
{
local $dbh=DBI->connect("DBI:mysql:$DB_NAME:$DB_HOST", 
$DB_USER, $DB_PASS, {RaiseError => 1});
#
такая обработка ошибок сократит код работы с БД
#
и сделает его более наглядным
eval {$result = exists($functions{$cmd}) ? 
$functions{$cmd}($params) : ""};
if($@) {$result="Error: $@";}
$client->send_im($uin, $result) if $result;
$dbh->disconnect();
}
}

# функция регулировки скорости в %
sub speed {
my ($result, $cmd, $speed);
my $request=$_[0];
#
определяем скорость:
$speed=int($request*$MAX_SPEED/100);
#
изменяем значение пропускной скорости канала 1:
$cmd="/sbin/ipfw pipe 1 config bw ".$speed."Kbit/s";
system($cmd) ;
$result=`/sbin/ipfw pipe show 1 | head -1`;
return $result;
}

# добавляем новую задачу в очередь закачек
sub htftp {
#
аргументы функции – ссылка и необязательное 
#
значение приоритета её закачки
my ($link, $prior)=split(/ /, $_[0]);
my $result;
$prior=int($prior);
my $sql="INSERT INTO download_queue 
(add_time, type, link, prior)
VALUES (NOW(), 'htftp', '$link', $prior)";
$dbh->do($sql);
return "Link added!";
}

# функция отображения очереди закачек
sub showlist {
my ($query, $id, $add_time, $link, $prior, $result);
#
задания находятся в состоянии ожидания, если не 
#
установлено время начала и окончания закачки
$query=$dbh->prepare("SELECT id, add_time, link, prior 
FROM download_queue 
WHERE start_time IS NULL AND end_time IS NULL
ORDER BY prior DESC, add_time");
$query->execute();
while(($id, $add_time, $link, $prior)=
$query->fetchrow_array()) {
$result.="$id\t$add_time\t$link\t$prior\n"
}
$result.="Queue is empty!" if($result=='');
return $result;
}

Полученный скрипт разместим в директории /usr/local/bin и назначим ему право на исполнение:

chmod +x /usr/local/bin/icq_bot.pl

Если у вас есть желание «демонизовать» этот скрипт, то добавьте следующий строки:


use POSIX;
#
«клонируем» себя
$pid=fork;
exit if $pid;
die "Ошибка fork: $!" unless defined($pid);
#
в порожденном процессе переназначаем 
#
дескрипторы ввода/вывода/ошибок
for my $handle (*STDIN, *STDOUT, *STDERR) {
open($handle, "+<", "/dev/null");
}
POSIX::setsid();
#
назначаем собственный обработчик функций:
$SIG{INT}=$SIG{TERM}=$SIG{HUP}=\&softexit;

sub softexit {
#
мягкое завершение ICQ-сессии
$oscar->signoff();
$quit=1;
}

Вызов «демонизированного» скрипта разместим в файле
rc.local:

echo “/usr/local/bin/icq_bot.pl”>>/etc/rc.local

Запустим полученный код и добавим нашего ICQ-бота себе в контакт лист:

Бот обрабатывает команды только с UIN`а, заданного в переменной $ADM_UIN. Кодом определено, что бот обрабатывает все строки, предваренные символом «!». Испытаем на нем команды:

Скрипт закачек


#!/usr/bin/perl

use DBI;

# конфигурационные данные
$DB_NAME="icq_bot";
$DB_HOST="localhost";
$DB_USER="user";
$DB_PASS="pass";
$WGET_PATH="/usr/local/bin/wget";

# функции закачки вызываются через хеш 
#
(по аналогии с кодом icq_bot.pl)
%functions = ("htftp" => \&htftp);

$quit = 0;
my ($id, $type, $link);
my $dbh=DBI->connect("DBI:mysql:$DB_NAME:$DB_HOST", 
$DB_USER, $DB_PASS, {RaiseError => 1});
# Continue downloading;
while(!$quit) {
eval { 
#
выбираем очередной элемент из очереди
my $sql="SELECT id, type, link 
FROM download_queue
WHERE start_time IS NULL AND end_time IS NULL
ORDER BY prior DESC, add_time LIMIT 1";
my $query=$dbh->prepare($sql);
$query->execute();
if($query->rows>0) {
($id, $type, $link)=$query->fetchrow_array();
print "Starting download $id $link $type";
#
делаем запись в БД о времени начала закачки
$sql="UPDATE download_queue 
SET start_time=NOW() WHERE id=$id";
$dbh->do($sql);
#
вызываем функцию-обработчик для закачек типа $type
$functions{$type}($link);
$sql="UPDATE download_queue 
SET end_time=NOW()
WHERE id=$id";
$dbh->do($sql);
return;
}

# если очередь пуста, 
#
то проверяем её каждые 10 секунд
`sleep 10`;
};
if($@) {
print "Error: $@\n";
}
} #end of main body
$dbh->disconnect();

# обработчик http и ftp закачек
sub htftp {
my $link=@_[0];
my $cmd="$WGET_PATH -c $link";
print $cmd;
system("$cmd");
exit $? if($?);
}

Подобно icq_bot.pl, можно «демонизировать» этот скрипт, но я решил поступить иначе – привязать вывод скрипта ddd.pl к виртуальному терминалу ttyv7, который вызывается нажатием комбинации клавиш Alt+F8.

Для начала разместим скрипт ddd.pl в домашней директории пользователя icq_bot. Там же создадим небольшой скрипт startddd.sh (не забудьте назначить ему права на исполнение):

#!/bin/sh
sudo -u icq_bot /home/icq_bot/ddd.pl

Далее, в файле /etc/gettytab добавим строки, описывающие новый тип терминала:

icqbot:ht:np:sp#115200:al=icq_bot:lo=/home/icq_bot/startddd.sh:

Здесь мы переопределили программу автозагрузки (по умолчанию /usr/bin/login) на наш скрипт.

Осталось переопределить тип седьмого виртуального терминала. Для этого в файле /etc/ttys добавим параметр icqbot к вызову
getty:

ttyv7 "/usr/libexec/getty icqbot" cons25r on secure

Все готово:

kill -1 1

 

Заключение

Предложенная мной система, как и любая другая, не идеальна, но имеет некоторый запас для дальнейшего развития:

  • наращивание числа протоколов, с которыми она может работать
  • повышение отказоусточивости
  • более гибкая приоритезация
  • управление временем закачек

Расширенную версию icq_bot.pl и ddd.pl вы можете скачать отсюда:

http://eterniya.ru/files/icq_bot.zip

Оставить мнение

Check Also

Используй, свободно! Как работает уязвимость use-after-free в почтовике Exim

В самом популярном на сегодняшний день почтовом сервере Exim был обнаружен опасный баг: ес…