вторник, 27 марта 2012 г.

Шпаргалка по настройке веб-сервера Lighttpd

Я давно пользуюсь веб-сервером Lighttpd и часто замечаю, что этот веб-сервер часто незаслуженно обходят стороной, отдавая предпочтение его более популярным коллегам nginx и Apache. У меня он давно заменил собой Apache, а об использовании nginx я даже не особо задумывался, потому что по принципу работы он похож на Lighttpd, а по возможностям, кажется, ни в чём ему не уступает.

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

Псевдонимы файлов и каталогов

Настройка псевдонимов каталогов. Часто веб-приложения устанавливаются в каталог, отличный от корня документов веб-сервера. Чтобы такие веб-приложения заработали, нужно включить и настроить модуль mod_alias:
server.modules  += ( "mod_alias" )
alias.url += (
  "/munin/" => "/var/cache/munin/www/",
  "/cacti/" => "/usr/share/cacti/site/",
  "/dokuwiki/" => "/usr/share/dokuwiki/",
  "/wordpress/" => "/usr/share/wordpress/",
  "/awstats/cgi-bin/" => "/usr/lib/cgi-bin/",
  "/awstats/docs/" => "/usr/share/doc/awstats/html/",
  "/awstats/icon/" => "/usr/share/awstats/icon/",
  "/awstats/css/" => "/usr/share/awstats/css/",
  "/awstats/classes/" => "/usr/share/awstats/classes/",
  "/postfixadmin/" => "/usr/share/postfixadmin/",
  "/mail/" => "/usr/share/squirrelmail/"
)
Чтобы выдавать пользователям с разными IP-адресами разное содержимое, можно воспользоваться условными блоками:
$HTTP["remoteip"] =~ "10.0.0." { alias.url += ( "/wpad.dat" => "/var/www/wpad-office.dat" ) }

$HTTP["remoteip"] =~ "10.0.1." { alias.url += ( "/wpad.dat" => "/var/www/wpad-agency.dat" ) }
Аутентификация при доступе к каталогу

Чтобы защитить некоторые каталоги сайта паролем, можно воспользоваться модулем mod_auth (я обычно использую бэкенд htdigest):
server.modules += ( "mod_auth" )

auth.backend = "htdigest"
auth.backend.htdigest.userfile = "/etc/lighttpd/htdigest"

auth.require = (
  "/RPC2" =>
  (
    "method" => "digest",
    "realm" => "rTorrent RPC",
    "require" => "user=rtorrent"
  ),
  "/stat/" =>
  (
    "method"  => "digest",
    "realm"   => "lightsquid statistics",
    "require" => "valid-user"
  ),
  "/postadmin/" =>
  (
    "method"  => "digest",
    "realm"   => "postadmin",
    "require" => "valid-user"
  )
)
Для управления файлом паролей в Debian можно установить пакет apache2-utils, в нём есть программы htpasswd для управления файлом паролей для бэкенда plain и htdigest для управления файлом паролей бэкенда htdigest.

Почитайте документацию - модуль mod_auth позволяет брать пользователей из каталога LDAP (в том числе Active Directory).

К примеру, вот так создаётся файл паролей с одним новым пользователем:
$ htdigest -c /etc/lighttpd/htdigest "realm text" user
Для добавления новых пользователей в уже существующий файл нужно воспользоваться той же командой, но без опции -c:
$ htdigest /etc/lighttpd/htdigest "realm text" user
Классический CGI

Для запуска классических CGI-скриптов можно воспользоваться модулем mod_cgi:
server.modules  += ( "mod_cgi" )
$HTTP["url"] =~ "^/cgi-bin/" { cgi.assign = ( "" => "" ) }

cgi.assign   = ( ".py"  => "/usr/bin/python", )
В этом примере опять используется условный блок, однако на сей раз условие зависит от URL запрошенного файла. В этом примере для всех файлов, находящихся в каталоге /cgi-bin/ веб-сервера, осуществляется их запуск. Выбор интерпретатора для скриптов осуществляется по первой строчке скрипта, она должна начинаться с символов #! (she-bang) с последующим полным путём к интерпретатору. Для остальных файлов с расширением .py осуществляется их запуск в интерпретаторе языка Python.

Модуль FastCGI

Протокол FastCGI является логическим продолжением идеи CGI. Процесс, работающий по этому протоколу, запускается однажды и обрабатывает несколько последовательных запросов без перезапуска. Этот подход позволяет сэкономить ресурсы на чтение сценария, его трансляцию во внутренне представление интерпретатора, на инициализацию структур данных, необходимых для обработки каждого запроса - все эти этапы выполняются только один раз - при запуске процесса.
server.modules   += ( "mod_fastcgi" )

fastcgi.server = (
  ".php" =>
  (
    (
      "host" => "10.0.0.1",
      "port" => 8080,
    ),
    (
      "bin-path" => "/usr/bin/php5-cgi",
      "socket" => "/tmp/php.socket",
      "max-procs" => 2,
      "idle-timeout" => 20,
      "bin-environment" =>
      (
        "PHP_FCGI_CHILDREN" => "4",
        "PHP_FCGI_MAX_REQUESTS" => "10000"
      ),
      "bin-copy-environment" =>
      (
        "PATH",
        "SHELL",
        "USER"
      ),
      "broken-scriptfilename" => "enable"
    )
  ),
  "/fcgi.pl" =>
  (
    (
      "bin-path" => "/var/www/fcgi.pl",
      "socket" => "/tmp/fcgi-pl.socket",
      "max-procs" => 2,
      "idle-timeout" => 20,
      "bin-copy-environment" =>
      (
        "PATH",
        "SHELL",
        "USER"
      ),
      "broken-scriptfilename" => "enable"
    )
  )
)
В этом примере обработкой файлов с расширением .php занимаются два бэкенда. Один бэкенд автономный, расположен на удалённом компьютере с IP-адресом 10.0.0.1 на порту 8080. Второй бэкенд управляется самим сервером Lighttpd, который порождает новые процессы FastCGI, когда они нужны, перезапускает их, когда они отработают заданные 10000 запросов и уничтожает их избыточное количество по истечении установленного времени простоя. Доступ к порождаемым процессам осуществляется через UNIX-сокеты.

Ещё один бэкенд является полноценным FastCGI-приложением на Perl. Он тоже управляется Lighttpd, доступ тоже осуществляется через UNIX-сокеты.

Для управления автономными FastCGI-серверами можно воспользоваться менеджером процессов из пакета spawn-fcgi, который умеет порождать, перезапускать и убивать процессы FastCGI. Для запуска этого менеджера процессов можно воспользоваться следующим сценарием инициализации:
#!/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-user1
PID=/var/run/spawn-fcgi-php-user1.pid
DAEMON=/usr/bin/spawn-fcgi
DAEMON_OPTS="-f /usr/bin/php-cgi -a 10.0.0.1 -p 8080 -u user1 -g user1 -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
Этот сценарий породит менеджер процессов для запуска файлов PHP от имени пользователя user1. FastCGI-бэкенд будет доступен на IP-адресе 10.0.0.1, на порту 8080.

Я предпочитаю использовать именно spawn-fcgi, а не php-fpm, т.к. первый является, на мой взгляд более надёжным, потому что в полной мере проповедует модульный подход к построению программного обеспечения. Похоже, я не одинок, и разработчики Debian придерживаются точно такого-же мнения, избегая включать php-fpm в стабильный дистрибутив Debian.

Хочу отметить, что выполнение сценариев PHP в режиме FastCGI не столь эффективно, каким могло бы быть. Это связано с тем, что файл PHP, хоть и не требует повторного запуска интерпретатора и трансляции кода выполняемого скрипта, но выполняется каждый раз от начала и до конца, включая код инициализации и деинициализации. При обработке каждого нового запроса заново открываются подключения к базам данных, осуществляется чтение файлов конфигурации, загрузка шаблонов HTML-страниц.

Чтобы проиллюстрировать сказанное, а заодно и закончить рассмотрение модуля mod_fastcgi, приведу пример простейшего FastCGI-приложения на Perl с использованием модуля CGI::Fast:
#!/usr/bin/perl -w

use strict;
use CGI::Fast;

# Инициализация: загрузка ресурсов,
# установка подключения к БД

my $counter = 0;
while(my $q = CGI::Fast->new)
{
  $counter++;
  print "Content-Type: text/html\n\n";
  print "Я обработал $counter запросов!\n";

  my %params = $q->Vars;
  while (my ($k, $v) = each %params)
  {
    print "$k = $v\n";
  }
}

# Закрытие ресурсов / подключений
Запросы обрабатываются строго поочерёдно. Для ускорения обработки запросов и нужен менеджер процессов, который породит оптимальное количество FastCGI-процессов и распределит между ними нагрузку. Он же должен убивать простаивающие процессы или перезапускать процессы, обработавшие определённое максимальное количество запросов - во избежание утечки памяти.

Пример на Perl взят отсюда: Связка Nginx + Пускач + FastCGI на Perl. На том же сайте можно почитать подробнее о том, что собой представляет FastCGI изнутри.

Модуль SCGI

SCGI похож на FastCGI и хотя менее популярен, всё же используется в некоторых приложениях. Например, программа rtorrent работает как сервер SCGI, позволяя управлять собой с использованием этого протокола. Этим пользуется, например, веб-интерфейс rutorrent. Пример их настройки можно посмотреть здесь: rtorrent + rutorrent. Краткий пример настройки модуля mod_scgi именно для этого случая приведён ниже:
server.modules += ( "mod_scgi" )

scgi.server = (
  "/RPC2" =>
  (
    "127.0.0.1" =>
    (
      "host" => "127.0.0.1",
      "port" => 5000,
      "check-local" => "disable",
      "disable-time" => 0  # don't disable scgi if connection fails
    )
  )
)
У веб-сервера Lighttpd имеется много других модулей, среди которых есть, например mod_rewrite, mod_proxy, о которых можно прочитать в документации, поставляемой в комплекте с самим веб-сервером или на вики-странице разработчиков: http://redmine.lighttpd.net/projects/lighttpd/wiki.

Материалы на русском языке:

суббота, 24 марта 2012 г.

Веб-интерфейс Postadmin для управления почтовым сервером

Обещал выложить свой веб-интерфейс для управления почтовым сервером, так вот - он тут: postadmin.tbz

Вы можете спросить: "Чем оно лучше PostfixAdmin?" У этих программ разное назначение. Если вам нужно средство для управления почтовым хостингом, то PostfixAdmin для вас. Если же вам нужно средство управления корпоративным почтовым сервером, то я считаю свою программу более подходящей для этого.

Чего нет в моей программе, в отличие от PostfixAdmin:
  • Разделения привилегий различных пользователей. Пользователь имеет только одни привилегии - администратора, а ограничение доступа делается средствами веб-сервера.
  • Нет квот на количество почтовых ящиков и нет квот на количество псевдонимов.
  • Нет настроек для сбора почты со внешних почтовых ящиков (fetchmail).
  • Нет настроек отсылки уведомлений об отсутствии пользователя (vacation).
  • Нет псевдонимов для доменов.
Что есть в моей программе такого, чего нет в PostfixAdmin:
  • Управление списками рассылок/псевдонимами/подписками (bcc) - в Postfixadmin есть только управление псевдонимами. Все эти функции совмещены в пределах одной таблицы и их удобно редактировать прямо со страницы почтового ящика.
  • Управление "чёрным списком" отправителей, почта от которых не принимается.
  • Управление "белым списком" адресатов, отправлять почту которым может пользователь с ограничениями на отправку. Отдельная категория доступа на отправку - это возможность отправлять на любые внешние адреса.
  • Синхронизация справочной информации с MS SharePoint Services 3.0.
  • Управление произвольными списками ограничений (при изменении конфигурации самого Postfix).
Ну и кроме того:
  • В моей программе шаблоны HTML-страниц лежат отдельно, поэтому дизайн программы не прибит к ней гвоздями и его можно менять.
  • По идее, в мою программу проще добавить новую функциональность, т.к. для создания новых виджетов есть типовые функции.
Скриншоты, чтобы дать представление об интерфейсе.

Список доменов и транспортов:

Редактирование домена и транспорта:

Список пользователей-почтовых ящиков:

Редактирование пользователя-почтового ящика:


Добавление нового пользователя-почтового ящика:

Таблица списков рассылок/псевдонимов/подписок:

Редактирование списков управления доступом SMTP-сервера:

Редактирование отдельного правила:

Синхронизация информации о пользователях с порталом MS SharePoint Services 3.0:

Более подробные (но не пошаговые и "на пальцах") инструкции по настройке находятся в самом архиве, в файле README.

Как я уже говорил здесь: http://morbow.blogspot.com/2012/03/blog-post.html, в дальнейшей разработке этого веб-интерфейса я пока не заинтересован. Если кого-то заинтересует этот веб-интерфейс, я готов ответить на вопросы по нему по почте wheelof@gmail.com. Если кто-то напишет для программы какую-то дополнительную функциональность - прошу поделиться, если не жалко :)

P.S. Переписал программу на Perl с использованием шаблонизатора HTML::Template и фреймворка Dancer. Почитать о ней можно тут: Postadmin 2.

понедельник, 19 марта 2012 г.

Запросы и обновления Active Directory

Эта статься взята из журнала Windows IT Pro/RE за июнь 2006 года. Текст статьи можно найти по ссылке http://www.osp.ru/win2000/2006/04/2578905/, однако там отсутствуют листинги программ - возможно они потерялись при изменении структуры сайта. Найти копии этой статьи с листингами в интернете мне не удалось, поэтому нашёл сам журнал (на торрентах) и набрал листинги сам. Если правообладатели выложат на сайте листинги, готов убрать статью со своего блога.

Чем меня привлекла эта статья? Неделю назад я написал на Perl небольшой скрипт с использованием Net::LDAP. Документация на английском очень хороша, но мне приятнее обращаться к документации как к справочнику, а усваивать что-то новое проще всё-таки на русском. Хорошим учебником по этой теме может послужить книга Дэвида Бланк-Эдельмана "Perl для системного администрирования", в которой есть глава, посвящённая использованию модулей Net::LDAP и Mozilla::LDAP. Недостаток главы из этой книги заключается в том, что там оба модуля рассматриваются параллельно и читать из-за этого её не очень удобно. В этой статье сжато рассмотрены все основные операции по работе с каталогом LDAP применительно только к модулю Net::LDAP.

Хочу ещё отметить, что я совсем не ожидал увидеть такую статью в журнале, целиком посвящённом использованию Windows. Perl, как мне кажется, чужд системным администраторам Windows.

Запросы и обновления Active Directory

Использование модулей Perl Net::LDAP

Для автоматизации и организации программного доступа к Active Directory (AD) обычно выбирают интерфейс API, называемый Microsoft Active Directory Service Interfaces, ADSI. ADSI достаточно прост в использовании и имеет понятный интерфейс, позволяющий легко управлять объектами в AD. Благодаря комбинации ADSI и ADO для осуществления запросов можно выполнять практически любые действия, автоматизирующие использование AD.

Поскольку ADSI построен на модели COM, разработанной и применяемой только Microsoft, воспользоваться ADSI на отличных от Windows платформах не удастся. Использование ADSI для разработки сценариев или приложений для других операционных систем, а также разработка независимых от платформы сценариев или приложений затруднены. К счастью, существует альтернатива: можно использовать Lightweight Directory Access Protocol (LDAP) и его API.

Записи журналов Microsoft для поддерживаемых стандартов не всегда хороши, но с появлением AD специалисты Microsoft внесли изменения, которые заметно улучшили положение. AD поддерживает не только LDAP, но и другие стандарты, такие как DNS, Simple Network Time Protocol (SNTP), Secure Sockets Layer (SSL), Transport Layer Security (TLS) и Kerberos. Одна из причин взаимодействия AD с LDAP состоит в том, что LDAP делает AD более независимой от платформы с точки зрения клиента. Поскольку LDAP является стандартом, вы не ограничены Windows-клиентами и Windows-платформой. LDAP существует уже достаточно давно и имеет клиентов практически для всех платформ. Это означает, что вы можете писать и использовать сценарии для клиентов, поддерживающих LDAP и осуществляющих запросы и обновления AD с выбранной платформы. Такая возможность будет полезна для администраторов, которым необходимо разрабатывать сценарии и приложения для отличных от Windows платформ, а также для тех, кто пишет кроссплатформенные приложения и сценарии, и пользующие AD.

Многие думают, что LDAP — это всего-навсего протокол. Но, в отличие от большинства стандартов для протоколов, LDAP имеет определенный стандартом IETF) RFC программный интерфейс API. Он описан в RFC 1823 (http://www.ietf.org/rfc/rfc1823.txt) и обычно именуется C-style LDAP API. API содержит базовый набор функций для осуществления запросов и обновления основанного на LDAP каталога. Специалисты Microsoft создали соответствующий пакет разработчика, SDK, который используется для работы с LDAP API.

C-style LDAP API, являющийся практически стандартным для использования с LDAP, поначалу кажется удобным. Его недостаток заключается в том, что он не является объектно-ориентированным и недостаточно хорошо взаимодействует с некоторыми языками, такими как Java. Поэтому Sun Microsystems разработала собственный LDAP API, известный как Java Naming and Directory Interface, JNDI (см. http://java.sun.com/products/jndi). JNDI обладает расширенными возможностями по сравнению с LDAP API. JNDI является также интерфейсом к DNS, по идеологии похожим на ADSI, и может использоваться в качестве общего интерфейса к службе каталога.

В сообществе разработчиков Perl были созданы наборы Perl-модулей для LDAP, в основе которых лежит Netscape LDAP SDK, более известный как PerLDAP. В имени модулей PerLDAP имеется префикс Mozilla::LDAP. К сожалению, такие модули Perl требуют для использования дополнительной установки Netscape LDAP SDK. Другая группа разработчиков создала основанную на чистом Perl реализацию таких же модулей Perl, известных как perl-ldap. По именам они не пересекаются с модулями Netscape PerLDAP. Модули perl-ldap именуются префиксом Net::LDAP. Большое преимущество модулей Net::LDAP заключается в том, что вы можете установить модули Net::LDAP практически на любую систему, поддерживающую Perl. В результате появляется возможность написания основанных на LDAP клиентов без использования каких-либо внешних SDK или дополнительного программного обеспечения. Модули Net::LDAP используют большинство имен функций C-style API, но в отличие от них являются объектно-ориентированными и более простыми в применении.

В этой статье я объясню, как установить и использовать модули Net::LDAP для осуществления запросов к AD. Поскольку статья адресована пользователям, имеющим опыт работы с Perl, я не буду описывать процедуру установки. Мы обсудим использование модулей Net::LDAP для создания и обновления объектов в AD.

Установка Net::LDAP

У опытных пользователей Perl установка модулей не вызывает затруднений. При использовании для установки Net:: LDAP среды Comprehensive Perl Archive Network (CPAN) можно выполнить следующую команду:
> perl -MCPAN -e shell
cpan> install Net::LDAP
Если вы никогда не использовали среду CPAN, рекомендую изучить ее возможности как можно тщательнее, поскольку это значительно упростит процедуру установки и обновления модулей. Модуль CPAN.pm поставляется со многими дистрибутивами Perl. Когда пользователь запускает этот модуль в первый раз, программа установки проводит его через установку среды CPAN. Инструкции о том, как установить модули CPAN на различные платформы, можно найти на сайтах, посвященных CPAN (например, http://www.cpan.org/modules/INSTALL.html).

Ссылки на последнюю версию библиотеки perl-ldap вместе с документацией есть на домашней странице perl-ldap (http://perl-ldap.sourceforge.net). Версия кода perl-ldap, использованного в этой статье, perl-ldap-0.26.

Начало работы с Net::LDAP

Начнем с простого примера использования модулей Net::LDAP. Листинг 1 содержит код, возвращающий основную информацию из каталога. В этой программе я задействовал процедуру new() для создания нового соединения с контроллером домена (DC), имеющим имя dc1.

Листинг 1. Код, показывающий информацию из RootDSE
# Программа, показывающая информацию из RootDSE
use strict;
use Net::LDAP;
my $ldap = Net::LDAP->new('dc1') or die $@;
my $rootdse = $ldap->root_dse(attrs => ['defaultNamingContext']);
print $rootdse->get_value('defaultNamingContext');
После этого я вызвал процедуру root_dse для получения нужных атрибутов из Root DSE, который является хранилищем (репозитарием) информации о контроллере домена DC. В этом примере мне нужно получить атрибуты, определяющие контекст именования домена, поэтому я установил значение параметра attrs в defaultNamingContext. Чтобы получить все атрибуты в Root DSE, необходимо установить значение параметра attrs в (*). Затем я использовал процедуру get_value() для получения значений нужных атрибутов. Например, значение, возвращаемое для домена mycorp.com будет dc=mycorp, dc=com.

Заметим, что листинг 1 не содержит какого-либо кода для аутентификации. Root DSE доступен по анонимному доступу, чтобы приложения имели некую стартовую точку, определив ее из основной информации о каталоге. Для выполнения более сложных запросов понадобится включить в приложение аутентификационный код, подобный показанному в листинге 2. В этом коде я вызывал процедуру bind() и задавал требуемое полное имя distinguished name (DN) и пароль для учетной записи, от имени которой хотел регистрироваться. Метод bind() возвращает объект — сообщение Net::LDAP::Message, который я использую для определения того, были ли случаи ошибок во время аутентификации. Если ошибки были, метод code() возвращает код ошибки, а метод error() возвращает текстовое сообщение о ней. Для окончания сессии авторизации я использую процедуру unbind().

Листинг 2. Код аутентификации
# Код аутентификации
use strict;
use Net::LDAP;
my $dc = 'dc1';
my $user = 'cn=administrator, cn=users, dc=mycorp, dc=com';
my $passwd = 'Adminpasswd';
my $ldap = Net::LDAP->new($dc) or die $@;
my $rc = $ldap->bind($user, password => $password);
die $rc->error if $rc->code;
$ldap->unbind;

Запросы к AD

Для тех, кому хорошо известны параметры, использующиеся для поиска в LDAP, предпочтительным будет поиск при помощи Net::LDAP. Для выполнения поиска в LDAP обычно задаются три параметра; начальное полное имя DN, границы (scope) и фильтр. Параметр base DN задает точку, с которой начинается поиск. Параметр scope (границы) определяет границы (диапазон) поиска. Можно использовать одно из следующих значений:
  • base - соответствует только объекту, который задан в начальном DN,
  • onelevel или one - соответствует объектам, расположенным на один уровень ниже начального DN (т. е. прямые потомки родителя), исключая начальный DN,
  • subtree или sub - соответствует любым объектам, расположенным ниже начального DN, исключая начальный DN.
Параметр filter является префиксом строки, определяющей критерии, которым должны соответствовать объекты. Синтаксис фильтра определен в RFC 2254 (http://www.ietf.org/rfc/rfc2254.txt). В таблице 1 показано несколько простых фильтров поиска.

Таблица 1. Примеры фильтров поиска
Фильтр поискаКритерий поиска
(&(objectclass=user)(objectcategory=Person))Все пользовательсякие объекты в AD
(&(objectclass=user)(objectcategory=Precon)(sn=Allen)) Все объекты типа пользователь, имеющие фамилию Allen
(&(objectclass=computer)(objectcategory=Computer)(name=w2k*))Все объекты типа компьютер, начинающиеся с w2k
(&(objectclass=contact)(objectcategory=Person)(!description=*)))Все объекты контакты, не имеющие заполненного атрибута Description
В листинге 3 показан код программы, выполняющей запросы к AD для поиска всех пользователей, фамилия которых Allen. Заметьте, что переменная $user расположена в программном коде, выделенном в блоке A. Вместо задания полного имени пользователя user DN, я задаю основное имя пользователя user principal name (UPN), которое является идентификатором в стиле электронной почты и которое пользователь вводит при регистрации. Соответствие пользовательских имен UPN адресам электронной почты — достаточно распространенная практика. Если DNS-имя леса не соответствует DNS-суффиксу, используемому в e-mail-адресах, можно создать дополнительный суффикс UPN, выполнив шаги, описанные в статье Microsoft «HOW TO: Add UPN Suffixes to a Forest» (http://support.microsoft.com/?kbid=243629). Если вы используете UPN, вам нет необходимости задавать в программе полное DN-имя пользователя.

Листинг 3. Программа, выполняющая запросы к AD
# Программа, выполняющая запросы к AD
use strict;
use Net::LDAP;
# BEGIN CALLOUT A
# BEGIN COMMENT
# Задание соединения и параметров регистрации
# END COMMENT
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
# BEGIN COMMENT
# Определение параметров поиска
# END COMMENT
my $base = "cn=users, dc=mycorp, dc=com";
my $scope = "subtree";
my $filter = "(&(objectclass=user)(objectcategory=user)(sn=Allen))";
my $ldap = Net::LDAP->new($dc) or die $@;
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# END CALLOUT A
# BEGIN CALLOUT B
my $search = $ldap->search(
  base => $base,
  scope => $scope,
  filter => $filter
);
die $search->error if $search->code;
# END CALLOUT B
# BEGIN CALLOUT C
foreach my $entry ($search->entries) {
  $entry->dump;
}
# END CALLOUT C
$ldap->unbind;
Как видно из программного кода в блоке B листинга 3, я использовал для осуществления запросов процедуру search(). Были заданы три параметра (т. е. base DN, scope и filter), которые мы рассмотрели выше. Дополнительно был задан параметр attrs, задающий массив атрибутов, которые необходимо вывести. Если не задать параметр attrs, операция поиска возвратит все атрибуты, имеющие значение. Если в результате поиска возвращается большое число атрибутов, возможно, кто-то захочет уменьшить количество возвращаемых данных, использовав для этого параметр.

Как видно из программного кода в блоке C листинга 3, я использовал процедуру entries() для последовательного отображения возвращаемых процедурой поиска данных. Этот метод возвращает массив объектов Net::LDAP::Entry. Для каждого из таких объектов я использовал процедуру dump() для вывода всех возвращаемых атрибутов. Если необходим доступ к определенному атрибуту, можно использовать процедуру get_value() так, как это было продемонстрировано в листинге 1.

Создание утилиты поиска в LDAP

Теперь, зная основы использования модулей Net::LDAP для выполнения операций поиска, давайте рассмотрим код простой утилиты командной строки ldapsearch.pl, показанной в листинге 4. При помощи этой утилиты можно производить запросы к AD. Ldapsearch.pl основывается на широко используемой утилите поиска в LDAP (ldapsearch), доступной в большинстве пакетов SDK для LDAP и серверов каталога LDAP (за исключением AD).

Листинг 4. Ldapsearch.pl
use strict;
use Net::LDAP;
use Net::LDAP::Filter;
use Getopt::Std;
# BEGIN CALLOUT A
# BEGIN COMMENT
# Получение и проверка заданных параметров.
# END COMMENT
my %opts;
getopt('bshpDwfa', \%opts);
my($err_str) = validate_options(\%opts);
if ($err_str) {
  print "ldapsearch.pl: $err_str\n";
  print usage();
  exit;
}
# END CALLOUT A
# BEGIN CALLOUT B
# BEGIN COMMENT
# Соединение, затем привязка к хосту
# END COMMENT
my $ldap = Net::LDAP->new($opts{h}, port => $opts{p} || 389) or die "$@\n";
my $rc->error if $rc->code;
# END CALLOUT B
# BEGIN CALLOUT C
# BEGIN COMMENT
# Выполнение поиска и отображение результатов
# END COMMENT
my $search = $ldap->search {
  base => $opts{b},
  scope => $opts{s},
  filter => $opts{f},
  attrs => [split /,/, $opts{a}],
);
die $search->error if $search->code;
foreach my $entry ($search->entires) {
  $entry->dump;
}
$ldap->unbind;
exit;
# END CALLOUT C
# BEGIN CALLOUT D
# BEGIN COMMENT
# Различные функции.
# END COMMENT
sub usage {
  return qq(
    usage: ldapsearch.pl [options]
    -b basedn     base dn for search
    -s scope      one of base,one, or sub (search scope)
    -h host       ldap server
    -D binddn     bind dn
    -w passwd     bind passwd (for authentication)
    -f filter     RFC-2254 compliant LDAP search filter
    -a attributes comma-separated list of attributes to retrieve
    -p port       port on ldap server
  );
}
sub validate_options {
  my $opts_ref = $_[0];
  foreach (qw(b s h D w f a)) {
    return("-$_ required") unless $opts_ref->{$_};
  }
  my $filter = Net::LDAP::Filter->new($opts_ref->{f});
  return("Bad search filter") unless ref $filter;
  return;
}
# END CALLOUT D
Основной код ldapsearch.pl по большей части не отличается от программного кода в листинге 3, кроме ключей командной строки, семь из которых являются обязательными. Вы уже знакомы с некоторыми из этих ключей: ключи -b, -s и -f задают, соответственно, параметры для поиска base DN (базовое полное имя), scope (границы, диапазон) и filter (фильтр). Дополнительные ключи -D и -w задают полное имя пользователя и пароль и применяются в целях авторизации. Ключ -h задает имя сервера LDAP. Ключ -a определяет атрибуты, которые нужно запросить. Необходимо задать атрибуты в виде списка, разделенного запятыми. Последний ключ -p является необязательным параметром. Рассмотрим его назначение.

Чтобы выполнить обработку ключей командной строки, я использовал модуль Perl с именем GetOpt::Std. Этот модуль обеспечивает базовую функциональность для обработки ключей. Поскольку большинство ключей обязательные, ldapsearch.pl включает в себя программный код, проверяющий наличие ключей. Этот программный код показан в блоке A листинга 4.

Блок B листинга 3 выделяет программный код, выполняющий установку соединения. Разница между этим кодом и кодом в листинге 2 — в использовании ключа port (-p). В командной строке можно использовать ключ -p для задания альтернативного порта, например 3268 для глобального каталога Global Catalog (GC). Если ключ -p не используется, сценарий по умолчанию задействует порт 389, который является стандартным портом LDAP.

Программный код в блоке C листинга 4 выполняет поиск и вывод соответствующих атрибутов. Этот программный код также похож на представленный в листинге 3, за исключением параметра attr в процедуре поиска, поскольку введен ключ -а, задающий атрибуты для вывода. Я использовал функцию split для включения в массив списка, разделенного запятыми.

На экране 1 показаны результаты работы сценария ldapsearch.pl. Первые три строки содержат параметры, используемые для запуска сценария из командной строки. Как видно по этим параметрам, я запускал сценарий для хоста dc1 (ключ -h). Сценарий выполняет поиск в контейнере cn=computers, dc=mycorp, dc=com (ключ -b) и в подконтейнерах (ключ -s) для всех объектов «компьютер», имеющих имя, начинающееся с app (ключ -f ). Результаты запроса определяют строки, следующие за параметрами. Поскольку я задал возвращение только атрибута cn для каждого из объектов (ключ -a ), результатом будет показ полного имени DN соответствующего объекта и его атрибута cn.

Экран 1. Результаты работы ldapsearch.pl
> perl ldapsearch.pl -b cn=computers,dc=mycorp,dc=com -s sub
-h dc1 -D rallen@mycorp.com -w MyPasswd
- "(&(objectclass=computer)(objectcategory=computer)(cn=app*)" -a cn
----------------------------------------------------------------------
dn:CN=APP01,CN=Computers,DC=mycorp,DC=com

cn:APP01
----------------------------------------------------------------------
dn:CN=APP02,CN=Computers,DC=mycorp,DC=com

cn:APP02
----------------------------------------------------------------------
dn:CN=APP03,CN=Computers,DC=mycorp,DC=com

cn:APP03
----------------------------------------------------------------------
dn:CN=APP04,CN=Computers,DC=mycorp,DC=com

cn:APP04

Добавление объектов

Использование Net::LDAP для добавления объектов имеет свои преимущества, например, листинг 5 содержит код, который добавляет объект John Doe. Блок A в листинге 5 выводит параметры, которые необходимо изменить для получения работающей программы. Для переменной $dc требуется задать контроллер домена, на котором будут выполняться операции добавления. Переменным $user и $passwd следует присвоить соответствующее имя и пароль, под которыми нужно подключиться к заданному контроллеру домена. Переменная $parent_dn должна содержать полное имя DN родительского контейнера, в котором предполагается разместить объект John Doe. Программный код после блока A в листинге 1 подключается к заданному контроллеру домена и использует введенные имя и пароль для соединения с ним.

Листинг 5. Программа, добавляющая объект Contact
# Программа, добавляющая объект Contact
use strict;
use Net::LDAP;
# BEGIN CALLOUT A
# BEGIN COMMENT
# Настройте для вашего окружения
# END COMMENT
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
my $parent_dn = "cn=users,dc=mycorp, dc=com";
# END CALLOUT A
# BEGIN COMMENT
# Соединение и аутентификация
# END COMMENT
my $ldap = Net::LDAP->new($dc) or die "$@\n";
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# BEGIN CALLOUT B
# BEGIN COMMENT
# Добавление объекта contact John Doe.
# END COMMENT
$rc = $ldap->add("cn=mycontact, $parent_dn",
  attrs => [
    objectclass => 'contact',
    displayName => 'John Doe',
    sn => 'Doe',
    givenName => 'John',
    telephoneNumber => '555-123-4567',
  ]);
# END CALLOUT B
if ($rc->code) {
  print "Add failed: ", $rc->error, "\n";
}
else {
  print "Add successful\n";
}
$ldap->unbind;
Код блока B листинга 5 вызывает процедуру add(). Первым параметром является полное имя DN нового добавляемого объекта. Второй параметр attrs указывает на массив ссылок, содержащий атрибуты, которые присваиваются новому объекту. Необходимо включить некоторые обязательные параметры, например objectClass, которые не имеют значения по умолчанию, или добавить методы обработки на случай ошибки. Например, чтобы добавить объект «пользователь», необходимо, как минимум, задать user для параметра objectClass и имя пользователя для параметра sAMAccountName.

Метод add() возвращает объект Net::LDAP::Message, включающий процедуру code(), которая позволяет определить, были ли ошибки. Когда метод code() объекта Net::LDAP::Message возвращает значение 0, это означает, что контакт успешно добавлен. Если же метод code() возвращает иное значение, следовательно, была обнаружена ошибка, и метод error() отобразит соответствующее сообщение.

Листинг 5 добавляет в AD только один объект. Этот код можно расширить для добавления тысяч объектов. Можно использовать модули Perl Database Interface (DBI) для осуществления запросов к базе данных и пополнения AD восстановленной информацией. DBI-модули есть на сайте CPAN (http://www.perl.com/CPAN-local/modules/by-module/DBI).

Удаление объектов

Удалять объекты, используя Net::LDAP, даже проще, чем добавлять их. Все, что необходимо для проведения операции, это ввести полное имя объекта DN, который требуется удалить при помощи процедуры Net::LDAP delete(). Например, листинг 6 содержит код, удаляющий все объекты контактов в организационном подразделении ou=Contacts, dc=mycorp, dc=com.

Листинг 6. Код удаления объектов
# Код удаления объектов
use strict;
use Net::LDAP;
# BEGIN CALLOUT A
# BEGIN COMMENT
# Настройте для вашего окружения
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
my $parent_dn = 'ou=Contacts, dc=mycorp, dc=com';
# END CALLOUT A
# BEGIN COMMENT
# Соединение и аутентификация
# END COMMENT
my $ldap = Net::LDAP->new($dc) or die "$@\n";
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# BEGIN CALLOUT B
# BEGIN COMMENT
# Найти все объекты типа контакт в родительском DN
# END COMMENT
my $search = $ldap->search(
  base => $parent_dn,
  scope => 'one',
  filter => "(objectClass=contact)",
  attrs => ['cn']
};
die $search->error if $search->code;
# END CALLOUT B
# BEGIN CALLOUT C
# BEGIN COMMENT
# Удалить все подходящие объекты
# END COMMENT
my $count = 0;
foreach my $entry ($search->entries) {
  $rc = $ldap->delete($entry->dn);
  if ($rc->code) {
    print "Delete failed for ", $entry->get_value('cn'), ": ", $rc->error, "\n";
  }
  else {
    $count++;
    print "Delete successful: " $entry->get_value('cn'), "\n";
  }
}
print "Total successfully deleted: $count=n";
# END CALLOUT C
$ldap->unbind;
Программный код блока A показывает параметры, которые необходимо задать. Программный код блока B листинга 6 осуществляет поиск объектов для удаления. Ранее мы рассмотрели, как используется метод search(), включая процесс установки каждого из параметров. В нашем случае мы устанавливаем параметр base для задания полного имени DN-контейнера, содержащего объекты, подлежащие удалению. Нам требуется удалить все объекты в этом контейнере, но не сам контейнер. Для этого присвоим параметру scope значение 'one', чтобы исключить начальное DN из поиска. Поиск будет вестись на уровне, следующем за начальным DN. Параметр filter содержит фильтр поиска, который задает просмотр объектов контактов. Параметр attrs содержит массив ссылок на отдельный атрибут 'cn'. Метод search() возвращает соответствующие объекты как массив объектов Net::LDAP::Entry.

Программный код блока C в листинге 6 использует процедуру объекта Net::LDAP::Entry dn() для получения DN для каждого из соответствующих объектов, так чтобы метод delete() мог удалить объект. Метод code() проверяет результат на предмет того, была ли обнаружена ошибка, и выводит соответствующее сообщение.

В некоторых случаях вместо удаления каждого из объектов в OU, возможно, понадобится удалить родительский OU и все его дочерние объекты за одну операцию. К сожалению, выполнить операцию удаления такого типа при помощи простой команды delete не удастся. Придется использовать расширение протокола Lightweight Directory Access Protocol (LDAP), называемое control, для информирования сервера об удалении отдельных контейнеров и всех его дочерних объектов. Применение расширения Net::LDAP controls выходит за рамки данной статьи и послужит темой для последующих публикаций. А если кому-то уже сейчас необходимо узнать о функциях controls, можно ознакомиться с документацией на сайте, посвященном perl-ldap (http://perl-ldap.sourceforge.net).

Изменение объектов

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

Подобно методам add() и delete(), метод modify() в качестве первого параметра имеет полное имя DN объекта, подлежащего изменению. Вторым является параметр, задающий тип выполняемой модификации. Метод modify() имеет несколько режимов работы и можно выбрать наиболее подходящий.

Параметр add. Этот параметр добавляет или устанавливает значение атрибута, который до этого не имел присвоенного значения. Можно использовать параметр add для добавления нового значения в атрибут, имеющий несколько параметров. Параметр add использует ссылки на хеш (функции хеша). Ключ хеша содержит имя атрибута, а значение ключа хеша содержит значение атрибута. Например, для того чтобы добавить атрибут mail и присвоить ему значение jdoe@mycorp.com, можно воспользоваться следующим программным кодом:
$ldap->modify($dn, add =>
  {mail => 'jdoe@mycorp.com'});
Параметр delete. Параметр удаляет определенное значение, связанное с атрибутом. Он также использует ссылку на хеш, содержащий соответствующие пары ключевых значений для удаления, подобно параметру add, либо ссылается на массив атрибутов, для которого нужно удалить все значения. Например, для удаления всех значений, связанных с атрибутами mail и displayname, можно воспользоваться следующим программным кодом:
$ldap->modify($dn, delete =>
  ['mail', 'displayname']);
Параметр replace. Параметр replace используется для изменения существующих параметров атрибута заданными значениями. Он задействует те же параметры, что и add. Так, например, если ввести неправильный адрес email при добавлении атрибута mail, можно изменить его на правильный следующим программным кодом:
$ldap->modify($dn, replace =>
  {mail => 'joe@mycorp.com'});
Параметр changes. Параметр используется для группировки набора параметров add, delete и replace в единый вызов. Параметр changes использует ссылки на массив, содержащий пары «имя параметра — значение». Например, для удаления значения, связанного с атрибутом mail, и добавления атрибутов givenname и sn, значения которых соответственно John и Doe, можно воспользоваться кодом:
$ldap->modify($dn,
  changes => [
    add => [givenname => "John"],
    add => [sn => "Doe"],
    delete => ['mail']]);
Листинг 7 использует параметры, о которых было рассказано, для изменения объекта «пользователь». Подобно методам add() и delete(), метод modify() возвращает объект Net::LDAP::Message, метод code() которого используется для определения факта возникновения ошибки.

Листинг 7. Программа, изменяющая атрибуты объекта "пользователь"
# Программа, изменяющая атрибуты объекта "пользователь"
use strict;
use Net::LDAP;
# Настройте для вашего окружения
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
my $dn = "cn=jdoe, cn=users, dc=mycorp, dc=com";
# Соединение и аутентификация
my $ldap = Net::LDAP->new($dc) or die "$@\n";
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# Изменение нескольких атрибутов
pring "Setting givename, sn and mail...\n";
$rc = $ldap->modify($dn,
  changes => [
    add => [givename => "Johnny"],
    add => [sn => "Doh"],
    add => [mail => 'jdoe@mycorp.com'],
  ]
);
die $rc->error if $rc->code'
print "Changing givename to John...\n";
$rc = $ldap->modify($dn,
  replace => {givename => "John" });
die $rc->error if $rc->code;
print "Deleting the mail attribute...\n";
$rc = $ldap->modify($dn,
  delete => ['mail']);
die $rc->error if $rc->code;
print "Setting the telephoneNumber and sn...\n";
$rc = $ldap->modify($dn,
  changes => [
    add => [telephoneNumber => '555-123-4567'],
    replace => [sn => 'Doe'],
  ]
);
die $rc->error if $rc->code;
print "\nModifications successful\n";
$ldap->unbind;

Переименование и перемещение объектов

Чтобы иметь полный набор возможностей для управления объектами AD, необходима функция переименования и перемещения объектов. Метод Net::LDAP moddn() позволяет выполнить оба названных действия. Подобно методам, описанным выше, метод moddn() также имеет в качестве первого параметра полное имя объекта (DN), который предполагается переименовать или переместить. Второй параметр может состоять из одного или нескольких.

Параметры newrdn и deleteoldrdn. В AD вы идентифицируете объект по его полному имени DN, которое включает относительное имя relative distinguished name (RDN). RDN определяет имя объекта. Возьмем, например, объект с полным именем DN cn=jdoe, cn=users, dc=mycorp, dc=com. В этом случае относительным именем, RDN, будет jdoe. Параметр newrdn можно использовать для присвоения объекту нового RDN. Значение newrdn должно включать не только имя объекта, (например, jsmith), но и его идентификатор (например, cn=).

Листинг 8 содержит программный код, переименовывающий объект user cn=jdoe, cn=users, dc=mycorp, dc=com в cn=jsmith, cn=users, dc=mycorp, dc=com. Блок A листинга 8 выделяет код, включающий параметр newrdn. Этот код содержит в себе и параметр deleteoldrdn. Установив параметр deleteoldrdn в 1 (т. е. true), вы тем самым удаляете объект, имеющий старое имя RDN. Если же не включать параметр deleteoldrdn или установить его значение в 0 (т. е. false), старый объект, будучи переименованным или перемещенным, останется. Для того чтобы избежать такой ситуации, следует использовать параметр deleteoldrdn со значением 1.

Листинг 8. Программа, переименовывающая и удаляющая объект
# Программа, переименовывающая и удаляющая объект
use strict;
use Net::LDAP;
# BEGIN COMMENT
# Настройте для вашей среды
# END COMMENT
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
my $dn = "cn=jdoe, cn=users, dc=mycorp, dc=com";
my $new_rdn = "cn=jsmith";
# BEGIN COMMENT
# Соединение и аутентификация
# END COMMENT
my $ldap = Net::LDAP->new($dc) or die "$@\n";
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# BEGIN CALLOUT A
# BEGIN COMMENT
# Переименование jdoe в jsmith
# END COMMENT
$rc = $ldap->moddn($dn, newrdn => $new_rdn, deleteoldrdn => 1);
# END CALLOUT A
if ($rc->code) {
  print "Error renaming user: ", $rc->error, "\n";
}
else {
  print "Succesfully renamed user\n";
}
$ldap->unbind;
Параметр newsuperior. Этот параметр применяется для перемещения объекта в другой родительский контейнер. Данным параметром задается для полного имени новый родительский контейнер. Например, листинг 9 содержит код, перемещающий все пользовательские объекты, атрибут department которых имеет значение Sales в OU Sales. Как показано в листинге 9, переменная $old_parent определяет родительский контейнер, в котором вы ведете поиск объектов, а переменная $new_parent задает родительский контейнер, в который предстоит перенести объекты. В листинге 9 в блоке B выделен код, ведущий поиск $old_parent для всех объектов пользователей с атрибутом department, равным Sales. Программный код в блоке C листинга 4 использует оператор foreach для перемещения всех подобных объектов в $new_parent.

Листинг 9. Программа, перемещающая объекты
# Программа, перемещающая объекты
use strict;
use Net::LDAP;
# BEGIN COMMENT
# Настройте для вашего окружения
# END COMMENT
my $dc = 'dc1';
my $user = 'administrator@mycorp.com';
my $passwd = 'Adminpasswd';
# BEGIN CALLOUT A
my $old_parent = "cn=users, dc=mycorp, dc=com";
my $new_parent = "ou=sales, dc=mycorp, dc=com";
# END CALLOUT A
# BEGIN COMMENT
# Подключение и аутентификация
# END COMMENT
my $ldap = Net::LDAP->new($dc) or die "$@\n";
my $rc = $ldap->bind($user, password => $passwd);
die $rc->error if $rc->code;
# BEGIN CALLOUT B
# BEGIN COMMENT
# Поиск всех объектов user, чей атрибут department = Sales
# END COMMENT
my $search = $ldap->search(
  base => $old_parent,
  scope => 'one',
  filter => "(&(objectclass=user)(objectcategory=Person)(department=Sales))",
  attrs => ['cn']
);
die $search->error if $search->code;
# END CALLOUT B
# BEGIN CALLOUT C
# BEGIN COMMENT
# Перемещение соответствующих пользователей в Sales OU
# END COMMENT
my $count = 0;
foreach my $entry($search->entries) {
  $rc = $ldap->modrdn($entry->dn,
    newrdn => 'cn=' . $entry->get_value('cn'),
    newsuperior => $new_parent,
    deleteoldrdn => 1);
  if ($rc->code) {
    print "Move failed for ", $entry->get_value('cn'), ": ", $rc->error, "\n";
  }
  else {
    $count++;
    print "Move successful: ", $entry->get_value('cn'), "\n";
  }
}
print "Total successfully moved: $count\n";
# END CALLOUT C
$ldap->unbind;
Итак, в этой статье мы рассмотрели, как использовать модули Net::LDAP для управления объектами в AD. При использовании этих модулей Perl можно выполнять все необходимые манипуляции для управления данными в AD.

Робби Аллен - Технический руководитель в компании Cisco Systems. MVP по Windows Server Directory Services. rallen@rallenhome.com