The OpenNET Project / Index page

[ новости /+++ | форум | wiki | теги | ]

Инструкции по настройке аутентификации с помощью LDAP (ldap auth linux pam passwd)


<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>
Ключевые слова: ldap, auth, linux, pam, passwd,  (найти похожие документы)
From: Дмитрий Музалевский <mage@vanbel.nursat.kz> Date: Sun, 12 Jan 2004 17:02:14 +0000 (UTC) Subject: Инструкции по настройке аутентификации с помощью LDAP Оригинал: http://www.linuxrsp.ru/artic/auth_ldap.html Инструкции по настройке аутентификации с помощью LDAP Автор оригинальной статьи: Simon Ritchie <Simon.Ritchie@net.ntl.com> Перевод: Дмитрий Музалевский <mage@vanbel.nursat.kz> Оригинал статьи может быть найден по адресу: http://www.openldap.org./lists/openldapsoftware/200010/msg0009 Настройка аутентификации с помощью LDAP довольно тяжела, главным образом по той причине, что информация, которая вам нужна разбросана во множестве документов и некоторые важные детали не представлены. В результате я провел неделю в тяжких трудах для того, чтобы продемонстрировать как добиться необходимого результата. Я работаю в Red Hat Linux. Результаты, в сравнении с прочими UNIX системами довольно хороши. Далее некоторая информация, которая может оказаться полезной для людей использующих MS Windows и другие операционные системы. Полезные ссылки: http://www.openldap.org сайт содержит как описание, так и <свободнораспространяемые> дистрибутивы openLDAP. Вводные разделы очень хорошо разъясняют принципы LDAP. OpenLDAP основывается на программной продукции Мичиганского университета. Университет публикует свое собственное руководство, которое содержит некоторую жизненно важную информацию, которая может помочь вам, если вы захотите написать собственную backend software. Это "The slapd and slurpd administrator`s guide". Ссылка: http://www.umich.edu/~dirsvcs/ldap/doc/guides/slapd/toc.html Книга "Implementing LDAP", написанная Mark Wilcox (Wrox publishing) дает хорошее представление о предмете. Она содержит детализированную информацию, которую я не смог найти в онлайн - документации, но недостаточную для желаемых мне действий. Главные принципы. Традиционно, аутентификация в UNIX происходит чтением записей в файлах /etc/passwd, /etc/shadow, /etc/group. Каждая строка в /etc/passwd описывает одного пользователя и содержит ряд разделенных двоеточиями значений, например: boris:x:1101:100:Boris Morris:/home/boris:/bin/bash Эти поля означают следующее: - имя пользователя <системное, прим Пер.> - пароль ("x" означает смотреть в /etc/shadow) - идентификатор пользователя (user id, uid) - идентификатор группы (group id, gid) - персональная информация (как правило, реальное имя пользователя) - домашняя директория пользователя - оболочка (shell), предоставляемая по умолчанию при входе пользователя в систему. По некоторым причинам, поле персональной информации названо "полем GECOS". Эквивалентные записи в /etc/shadow: boris:<зашифрованный пароль>:11226:0:99999:7:-1:-1:134538484 Помимо пароля здесь хранится информация вроде последней даты смены пароля, которая используется системой для регулярных напоминаний пользователям о необходимости сменить пароль <в случае, если подобная функция в системе настроена, прим Пер.> Для входа в систему, Борис вводит свое пользовательское имя (boris) и свой пароль открытым текстом. Операционная система производит аутентификацию путем шифрования введенного пароля и сравнения результата с хранящимся в /etc/shadow. Зашифрованный пароль может быть расшифрован, но только суперпользователь root может читать файл /etc/shadow. Многие приложения читают информацию из файла /etc/passwd. Для примера: файловая система маркирует файл значением пользовательского идентификатора (uid) владельца, а не его именем. Команда "ls" выдает информацию о файлах, включая пользовательское имя владельца <с ключом "-l">. Для того, чтобы сделать это, она читает файл /etc/passwd и переводит uid в пользовательское имя. Каждый файл маркирован также идентификатором группы (gid). Это сделано для возможности коллективного доступа к нему. Файл может быть доступен для владельца, группы к которой он принадлежит и всех прочих пользователей <разумеется, с разными уровнями для перечисленных категорий, по параметрам - чтение, запись, исполнение, прим Пер.>. Если файл маркирован gid`ом группы "users", то он будет доступен для этой группы, а также для Бориса <объяснение почему - ниже>. Файл /etc/group содержит записи наподобие следующей: users::100: подразумевается, что группа "users" имеет gid со значением 100. Борис является членом этой группы, так как gid с этим значением указан в его идентификационной записи в файле /etc/passwd. Любая операционная система, производящая аутентификацию наподобие UNIX нуждается в хранении такого же набора данных - пользовательских имен, пользовательских идентификаторов, имен групп, идентификаторов групп. Подобное существует и в Windows NT, но есть разница в структуре аутентифицирующих данных. Система, которая производит аутентификацию и для UNIX и для Windows нуждается в хранении всех данных, используемых обеими системами. РАМ. Обычно, каждая составляющая операционной системы, требующая аутентификации (telnet server, ftp server, web server и т.д. и т.п.) имеет специальный встроенный код. В нынешнее время мы используем подключаемые модули аутентификации ("Pluggable Authentication Modules, PAM"). Для того, чтобы произвести аутентификацию с помощью РАМ - модуля, операционная система вызывает его посредством системной библиотеки. В LDAP - окружении, РАМ - модуль используется LDAP - сервером для аутентификации пользователей. РАМ поддерживается UNIX и Windows системами и его модули доступны из командной оболочки. Возможность производить аутентификацию с помощью РАМ имеется в Linux по умолчанию и существует набор модулей для telnet, ftp, и т.п. Существует также инструкция пользователя РАМ в HTML формате, но она хорошо спрятана <это, я сказал бы - для кого как, прим Пер.>. В дистрибутиве Linux, который я использую, она находится по адресу /usr/doc/pam - 0.66/html. РАМ LDAP - модуль не является частью Linux PAM <в последних версиях Red Hat Linux это не так, прим Пер.>. Он доступен на [58]www.padl.com. Далее приведен образчик аутентифицирующего модуля РАМ. Для примера служит команда "su", которая позволяет UNIX пользователю совершать какие - то действия от имени другого пользователя. Для того, чтобы использовать такую возможность, вам надо ввести пользовательское имя и его пароль, затем произойдет аутентификация. Вот РАМ - модуль /etc/pam.d/su: #%PAM - 1.0 auth sufficient /lib/security/pam_ldap.so auth required /lib/security/pam_unix_auth.so use_first_pass account sufficient /lib/security/pam_ldap.so debug account required /lib/security/pam_unix_acct.so password required /lib/security/pam_cracklib.so password sufficient /lib/security/pam_ldap.so password required /lib/security/pam_pwdb.so use_first_pass session required /lib/security/pam_unix_session.so (В разных системах этот файл может выглядеть по разному, хотя общее сходство, конечно останется, прим Пер.) Этот модуль использует разные уровни. Строки "auth" подразумевают актуальную аутентификацию. В первой строке предписывается использовать для аутентификации системную библиотеку pam_ldap.so. Она использует пользовательское имя и пароль. Если LDAP - сервер доступен, то проверяется введенный пароль, если нет, то предписание сбрасывается и в силу вступает модуль указанный во второй строке. Он принимает пользовательское имя и пароль принятые первым модулем (поскольку указан аргумент "use_first_pass" и проверяет их в локальном файле паролей /etc/passwd. Это очень полезный механизм, предохраняющий от неприятных неожиданностей, которые могут произойти в случае тестирования нового LDAP - сервера. Ключевой аргумент "sufficient" во второй строке указывает, что в случае успешной аутентификации по LDAP - паролю, вызов других модулей аутентификации не требуется. Если LDAP - сервер отвергает пароль, то попытка использования команды "su" оканчивается неудачей. РАМ - модуль задействует локальный файл паролей /etc/passwd лишь в случае недосягаемости LDAP - сервера. Если вы уберете вторую строку, то аутентификация будет происходить лишь на LDAP - сервере и в случае, если он не запущен <или не досягаем по каким - либо другим причинам>, то вы не сможете использовать команду "su". Другие строки предписывают реакции на события, которые могут случиться в другое время: например, когда пользователь пытается сменить свой пароль. Вы должны крайне осторожно редактировать такие РАМ - модули, которые не могут быть сброшены <в пользу других> и оставят вас бессильными. Оставляйте запасное залогиненное окно <консоль> суперпользователя root в то время когда совершаете такие действия. Проверьте наверняка, что вы все еще можете войти в систему как root, прежде чем остановить ее. В противном случае вы рискуете невозможностью входа в нее, когда в следующий раз запустите ее. Справочные страницы РАМ объясняют что предпринять в таком случае, но вы должны предварительно распечатать их, так как в случае невозможности входа в систему вы не сможете прочитать их в электронном виде. (В случае использования системы Red Hat Linux, решение вышеописанной проблемы, как правило легче всего решить с помощью загрузки операционной системы в однопользовательском режиме (если такая возможность не заблокирована вами или вашим системным администратором), заново отредактировать РАМ - модули и перезагрузиться, прим Пер.) Когда я проинсталлировал модуль РАМ LDAP, то поначалу он не работал, поскольку система не могла обнаружить его библиотеки. Система хранит список путей, по которым ищутся системные библиотеки. Эти пути описаны в файле /etc/ld.so.conf. Я добавил туда такую строку: /usr/local/lib туда проинсталлировались библиотеки моего PAM LDAP, затем выполнил команду: /sbin/ldconfig -v Это <действие по обновлению путей поиска системных библиотек посредством перечитывания файла /etc/ld.so.conf, прим Пер., прим Пер.> совершается при каждой перезагрузке, но приятнее обойтись без нее. Если вы используете другую UNIX систему, отличную от Linux, то возможно вам понадобятся другие действия. Смотрите справочную информацию по ldconfig (если такая команда есть в вашей системе). Теперь вы имеете исходно проинсталлированный РАМ LDAP - модуль, но использовать его не просто. В последней части данного руководства я опишу вам созданные мною способы для демонстрации того, как он работает. LDAP. LDAP это обслуживаемая база данных, оптимизированная для использования в качестве сервера каталогов. Аутентификация - одна из задач для которой она была разработана <я бы предпочел несколько другие определения, ну да и ладно, прим Пер.>. LDAP - сервер может функционировать на системе удаленной от той, в которую вы можете пытаться войти, возможно даже на другом сайте <всемирной сети, прим Пер.>, а может быть и на том же компьютере. Сервер получает запросы со всей сети и отвечает на них. Для аутентификации UNIX логинов вы можете создать отличный комплекс данных. Данные импортируются и экспортируются в LDIF формате. Этот формат подразумевает определенный набор атрибутов и значений. База данных LDAP иерархична. Каждый набор данных принадлежит домену, который в свою очередь входит в состав интернет - домена. Мой домен - "home.sys " существует лишь внутри моей домашней сети. Атрибуты, поддерживаемые базой данных LDAP описаны в наборе так называемых схем. Схема для атрибута, определяемая данным форматом это текст, переменная и т.д. в том же духе. Существует готовый для употребления набор схем для аутентификации, но они НЕ импортированы по умолчанию. Если ваш LDIF файл содержит атрибут не описанный в схеме, которую вы импортировали, данные не будут загружены. Некоторые схемы ошибочны, как это объяснено ниже. Конфигурационный файл slapd.conf определяет различные данные, включая схемы для импортирования, суперпользователя root (для базы данных LDAP) и пароль пользователя. В реальной версии пароль необходимо шифровать. В моей изолированной тестируемой системе я использую открытый текст. Я создал фиктивного пользователя "noris", который будет суперпользователем root для LDAP - сервера. В конфигурационном файле строка, начинающаяся с # является комментарием. Комментарии, помеченные SAR, являются лично моими: # $OpenLDAP: pkg/ldap/servers/slapd/slapd.conf,v 1.8.8.4 2000/08/26 17:06:18 kurt Exp $ # # See slapd.conf(5) for details on configuration options. # This file should NOT be world readable. # include /usr/local/etc/openldap/schema/core.schema include /usr/local/etc/openldap/schema/cosine.schema include /usr/local/etc/openldap/schema/nis.schema # Define global ACLs to disable default read access. # Do not enable referrals until AFTER you have a working directory # service AND an understanding of referrals. #referral ldap://root.openldap.org pidfile /usr/local/var/slapd.pid argsfile /usr/local/var/slapd.args # log function calls (1) and connection mgmnt (8) loglevel 9 # I don't know what this next bit is about - I can't find these files in # my OpenLDAP distribution. SAR 21 Sept 2000. # Load dynamic backend modules: # modulepath /usr/local/libexec/openldap # moduleload back_ldap.la # moduleload back_ldbm.la # moduleload back_passwd.la moduleload back_shell.la #--------------------------------- # ldbm database definitions #--------------------------------- database ldbm #suffix "o=My Organization Name, c=US" #rootdn "cn=Manager, o=My Organization Name, c=US" suffix "o=home, c=sys" rootdn "cn=noris, o=home, c=sys" # Cleartext passwords, especially for the rootdn, should # be avoid. See slappasswd(8) and slapd.conf(5) for details. # Use of strong authentication encouraged. rootpw n0risn # The database directory MUST exist prior to running slapd AND # should only be accessable by the slapd/tools. Mode 700 recommended. directory /usr/local/var/openldap-ldbm # Indices to maintain index objectClass eq Далее следует конфигурационный файл ldap.conf <не путать с предыдущим; если все было проинсталлировано в /usr/local, то будет находиться в /usr/local/etc>. Обратите внимание на закомментированную строку, в которой указан зашифрованный пароль: # $OpenLDAP: pkg/ldap/libraries/libldap/ldap.conf,v 1.4.8.6 2000/09/05 17:54:38 kurt Exp $ # # LDAP Defaults # # See ldap.conf(5) for details # This file should be world readable but not world writable. #BASE dc=example, dc=com #URI ldap://ldap.example.com ldap://ldap-master.example.com:666 #SIZELIMIT 12 #TIMELIMIT 15 #DEREF never # Не знаю точно, что это дает. Изменение не дало каких - либо результатов. (SAR) directory /usr/local/var/openldap-ldbm # Указанные данные должны совпадать с указанными в slapd.conf suffix "o=home, c=sys" rootdn "cn=noris, o=home, c=sys" rootpw n0risn # rootpw {crypto}$1$.Hl1/zRt$k9a.62WXw7i7GnL.RUbqZ/ index cn, sn, uid, gidnumber pres, eq, approx index objectclass pres,eq dbcachesize 500000 index default none Существует набор утилит на сайте http://www.padl.com , который сконвертирует данные из файлов /etc/passwd, /etc/shadow, /etc/group в файлы данных LDIF формата. Затем вы сможете импортировать их в вашу базу данных. Есть, однако несколько атрибутов, которые вы можете применить прямо сейчас. Это домен и высокоуровневые классы данных "Groups" и "People". Я определил их в файле "init.ldif". # initial attributes for LDAP authentication database # Specify root value, Group and People. We can then import the # attributes from /etc/group, /etcpasswd and /etc/shadow. dn: o=home, c=sys objectclass: top objectclass: organization o: home dn: ou=Group, o=home, c=sys objectclass: top objectclass: organizationalUnit ou: Group dn: ou=People, o=home, c=sys objectclass: top objectclass: organizationalUnit ou: People Теперь мы можем импортировать данные группы. Атрибуты для группы "users" будут похожи на следующее: dn: cn=users,ou=Group,o=home,c=sys objectClass: posixGroup objectClass: top cn: users gidNumber: 100 И вот, в конце концов мы можем импортировать данные о пользователях. Описание для пользователя boris будет таким: dn: uid=boris,ou=People,o=home,c=sys uid: boris cn: Boris Morris objectClass: account objectClass: posixAccount objectClass: top objectClass: shadowAccount userPassword: {crypt}$1$VCBun4.2$CHSPciCw.tkoI1McHIMYo/ shadowLastChange: 11226 shadowMax: 99999 shadowWarning: 7 shadowFlag: 134538484 loginShell: /bin/bash uidNumber: 1101 gidNumber: 100 homeDirectory: /home/boris gecos: Boris Morris Конечно, есть некоторое несоответствие между некоторыми данными и схемами. В данных для пользователя boris в файле /etc/shadow, значения shadowExpire и shadowInactive оба установлены как -1, но в схеме сказано, что они не могут быть отрицательными. Корректное решение заключается в использовании верной схемы. Как быстрое решение, я просто убрал эти строки из данных. Итак, мы включили пользователя boris в группу пользователей в домене home.sys. У нас может быть более чем один домен и в каждом мы можем завести такого пользователя с различными атрибутами - один LDAP - сервер может обслуживать несколько независимых доменов. Далее я привожу скрипт, с помощью которого я создаю собственно базу данных LDAP. В целях отладки он помещает данные о группе и пароле в разные файлы: #!/bin/bash # script to build an LDAP authentication database from /etc/group, # /etc/passwd and /etc/shadow. Must be run as root so that # migrate-passwd.pl can read /etc/shadow. Server must not already be # running - caches database, so clearing out the files isn't enough. # Stop server, clear any existing database and start server. kill -TERM `cat /usr/local/var/slapd.pid` rm -fr /usr/local/var/openldap-ldbm/* /usr/local/libexec/slapd sleep 10 # server takes a short while to be ready # Import initial attributes /usr/local/bin/ldapadd -f init.ldif -D "cn=noris, o=home, c=sys" -w n0risn # Import groups /usr/local/bin/ldap/migrate_group.pl /etc/group >group.ldif /usr/local/bin/ldapadd -f group.ldif -D "cn=noris, o=home, c=sys" -w n0risn # Import passwd. (Imports shadow automatically when run by root). # Remove any shadowInactive attributes with a negative value. The # schema is faulty and doesn't allow them. /usr/local/bin/ldap/migrate_passwd.pl /etc/passwd | fgrep -v "shadowExpire: -" | fgrep -v "shadowInactive: -" >passwd.ldif /usr/local/bin/ldapadd -f passwd.ldif -D "cn=noris, o=home, c=sys" -w n0risn Для тестирования моей свежесозданной базы LDAP я могу задать поиск пользователя boris: /usr/local/bin/ldapsearch -b "o=home, c=sys" "uid=boris" Получаю в ответ: version: 2 # # filter: uid=boris # requesting: ALL # # boris,People,home,sys dn: uid=boris,ou=People,o=home,c=sys uid: boris cn: Boris Morris objectClass: account objectClass: posixAccount objectClass: top objectClass: shadowAccount userPassword:: e2NyeXB0fSQxJFZDQnVuNC4yJENIU1BjaUN3LnRrb0kxTWNISU1Zby8= shadowLastChange: 11226 shadowMax: 99999 shadowWarning: 7 shadowFlag: 134538484 loginShell: /bin/bash uidNumber: 1101 gidNumber: 100 homeDirectory: /home/boris gecos: Boris Morris # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1 Имея установленную базу данных, я сменил пароль для boris используя команду passwd. Зашифрованный пароль в /etc/passwd теперь был отличен от хранящегося в базе LDAP. Я заменил РАМ - модуль, который обслуживал команду "su" на версию РАМ - LDAP и попытался применить эту команду к boris, она сработала. Далее, я ввожу новый пароль - аутентификация неудачна. В конце концов я остановил сервер LDAP. В этот раз новый пароль работает, а старый не проходит. Итак: если сервер LDAP запущен, вы можете использовать LDAP - пароль, если остановлен - можно воспользоваться паролем из локального файла паролей /etc/passwd. Таким образом, имея работающую базу LDAP меняйте модули аутентификации в /etc/pam.d один за другим на версии LDAP, тестируя каждый запускаемый вами сервис. Написание бэкэнда. Основано на информации, базирующейся на документах Мичиганского Университета. Бэкэнд позволяет интерпретировать очереди LDAP и сбрасывать данные для конвертации в ваш собственный код. Эта функция разработана для предоставления вам возможности создавать другие виды баз данных, использующих LDAP - сервер в качестве фронт - энда. LDAP - сервер организует взаимодействие бэкэнда между своими стандартными каналами ввода и вывода. Для тестирования этих возможностей я написал набор shell - скриптов для для использования их как бэкэнд программ. Я добавил очереди в slapd.conf для вызова их в каждой базе в домене home.sys: #---------------------------- # shell backend definitions #---------------------------- database shell suffix "o=home, c=sys" bind /home/simon/ldap/shdb/bind unbind /home/simon/ldap/shdb/unbind search /home/simon/ldap/shdb/search compare /home/simon/ldap/shdb/compare modify /home/simon/ldap/shdb/modify modrdn /home/simon/ldap/shdb/modrdn add /home/simon/ldap/shdb/add delete /home/simon/ldap/shdb/delete abandon /home/simon/ldap/shdb/abandon Все скрипты одинаковы (используют символические ссылки на один скрипт). Все они очень просты и используются лишь для аутентификации boris не проверяя пароль. #! /bin/bash # testbed script to respond to OpenLDAP shell backend requests. Just # print name and args, then return a result that says we've worked. # Authentication will work regardless of password and we will see results # in the log file. log=/home/simon/ldap/shdb/Log op=`basename $0` echo `date`: $0 ${op} $* >>${log} echo DEBUG: `date`: $0 ${op} $* case ${op} in search) cat >>${log} echo >>${log} echo dn: cn=boris,o=home,c=sys >>${log} echo dn: cn=boris,o=home,c=sys echo cn: boris >>${log} echo userPassword: encrypted.string echo userPassword: encrypted.string >>${log} echo cn: boris echo cn: Boris Norris >>${log} echo cn: Boris Norris echo sn: boris >>${log} echo sn: boris echo uid: boris >>${log} echo uid: boris echo >>${log} echo echo RESULT >>${log} echo RESULT echo code: 0 >>${log} echo code: 0 exit 0 ;; bind) cat >>${log} echo >>${log} echo RESULT >>${log} echo RESULT echo code: 0 >>${log} echo code: 0 exit 0 ;; unbind) # don't need to respond to an unbind request cat >>${log} exit 0 ;; *) cat >>${log} exit 0 ;; esac exit 0 Запустив такую конфигурацию LDAP, я могу прокрутить все, что делает РАМ - модуль. Я ввожу "su boris" в командной строке и обеспечиваю в открытом виде пароль "password for boris". РАМ - модуль сначала производит поиск подобного пользователя (существует ли он) и выдает все детали. Результат включает в себя зашифрованный пароль "encripted.string", но РАМ - модуль игнорирует его. Это выглядит для процедуры BIND так, как будто этот пользователь ввел пароль в открытом виде, как я это сделал. LDAP - сервер проверяет пароль перед тем как разрешить процедуры BIND и если они успешны, то пароль проходит корректно. Если пользователь хочет сменить свой пароль, соответствующий РАМ - модуль вызывает процедуру BIND и производит обновление перед завершением ее работы. Ниже находится лог аутентификации, снабженный аннотациями (помечены "<-"): Wed Oct 4 12:49:47 BST 2000: /home/simon/ldap/shdb/search search SEARCH msgid: 2 suffix: o=home,c=sys base: o=home, c=sys scope: 2 deref: 0 sizelimit: 1 timelimit: 0 filter: (uid=boris) <- поиск детальной информации по пользователю boris attrsonly: 0 attrs: all dn: cn=boris,o=home,c=sys <- backend выдает информацию по boris cn: boris userPassword: encrypted.string cn: Boris Norris sn: boris uid: boris RESULT code: 0 Wed Oct 4 12:49:47 BST 2000: /home/simon/ldap/shdb/bind bind BIND msgid: 3 suffix: o=home,c=sys dn: cn=boris,o=home,c=sys <- сервер шлет имя пользователя ... method: 128 credlen: 18 cred: password.for.boris <- ... и пароль RESULT <- backend шлет результат bind (OK) code: 0 Wed Oct 4 12:49:48 BST 2000: /home/simon/ldap/shdb/unbind unbind UNBIND msgid: 5 suffix: o=home,c=sys dn: Запросы, включая запрос BIND происходят с вызовом бэкэнд - скриптов, как я могу видеть из лога. РАМ - модуль доставляет пароль "password for boris" LDAP - серверу, затем отправляется BIND - скрипту, BIND - скрипт возвращает нулевой код, который означает "успех". Этот результат отправляется LDAP - серверу, тот в свою очередь отправляет соответствующий ответ РАМ - модулю. Как конечный результат всего этого, РАМ - модуль позволяет мне получить права boris. <Прошу прощения за несколько неудобоваримый стиль последних абзацев, но я старался делать как можно более буквальный перевод, так что оставьте это на совести автора, прим Пер.> Для этого простого тестирования я использовал shell - скрипты, но бэкэнды можно писать на таких языках как Perl, C, Java и тому подобных. Они лишь читают стандартные потоки ввода - вывода и реагируют соответственно.

<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>

 Добавить комментарий
Имя:
E-Mail:
Заголовок:
Текст:




Спонсоры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2022 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру