Если вы занимаетесь поддержкой веб-сервера или отвечаете за работу веб-сайта, простого или сложного, то наверняка регулярно решаете какие-то повторяющиеся задачи, такие как выявление недействительных внутренних или внешних ссылок. Многие из этих задач можно автоматизировать с использованием сценариев командной оболочки. То же касается некоторых типичных клиент/серверных задач, таких как управление доступом к информации в каталогах веб-сервера с использованием паролей.
Несколько сценариев в главе 7 продемонстрировали отдельные возможности текстового веб-браузера lynx, но в этой замечательной программе скрыто намного больше. Одна из таких возможностей, особенно полезная для администраторов веб-серверов, — функция traverse (включается флагом -traversal), заставляющая lynx опробовать все ссылки на сайте и отыскать среди них недействительные. Эту функцию можно задействовать в коротком сценарии, как тот, что показан в листинге 9.1.
Листинг 9.1. Сценарий checklinks
#!/bin/bash
# checklinks -- проверяет все внутренние ссылки на веб-сайте, сообщает
# о любых ошибках в файле "traverse.errors".
# Удалить по завершении все служебные файлы, созданные программой lynx.
trap "$(which rm) -f traverse.dat traverse2.dat" 0
if [ -z "$1" ] ; then
echo "Usage: checklinks URL" >&2
exit 1
fi
baseurl="$(echo $1 | cut -d/ -f3 | sed 's/http:\/\///')"
lynx -traversal -accept_all_cookies -realm "$1" > /dev/null
if [ -s "traverse.errors" ] ; then
/bin/echo -n $(wc -l < traverse.errors) errors encountered.
echo Checked $(grep '^http' traverse.dat | wc -l) pages at ${1}:
sed "s|$1||g" < traverse.errors
mv traverse.errors ${baseurl}.errors
echo "A copy of this output has been saved in ${baseurl}.errors"
else
/bin/echo -n "No errors encountered. ";
echo Checked $(grep '^http' traverse.dat | wc -l) pages at ${1}
fi
if [ -s "reject.dat" ]; then
mv reject.dat ${baseurl}.rejects
fi
exit 0
Основная работа в этом сценарии выполняется программой lynx ; сам сценарий просто играет с файлами, которые создает lynx, извлекая из них информацию и отображая ее в удобочитаемом виде. В выходной файл reject.dat программа lynx записывает ссылки с внешними адресами URL (см. ниже сценарий № 70, который использует этот файл), в файл traverse.errors — недействительные ссылки (цель данного сценария), в файл traverse.dat — список всех проверенных страниц, и в файл traverse2.dat — тот же список страниц, что и в файл traverse.dat, но с дополнительно включенными заголовками всех исследованных страниц.
Команда lynx поддерживает большое количество разных аргументов, и в данном случае нам потребовалось использовать -accept_all_cookies , чтобы программа не замучила нас вопросами — принимать или нет cookie от страницы. Мы также использовали аргумент -realm, чтобы проверке подвергались только страницы указанного уровня на сайте и «ниже», а не все ссылки, которые будут встречены на пути. Без аргумента -realm программа lynx могла бы отыскать тысячи и тысячи страниц. Мы попробовали выполнить функцию -traversal для адреса http://www.intuitive.com/wicked/ без -realm, и она обнаружила более 6500 страниц после более чем двухчасовой работы. С флагом -realm было найдено 146 страниц, на исследование которых ушло несколько минут.
Чтобы запустить сценарий, просто передайте ему адрес URL в командной строке. Сценарий способен выполнить анализ любого веб-сайта, но имейте в виду: проверка таких гигантов, как Google или Yahoo!, может затянуться навечно и закончиться исчерпанием места на вашем диске.
Давайте проверим маленький веб-сайт на наличие ошибок (листинг 9.2).
Листинг 9.2. Проверка веб-сайта, не имеющего ошибок, с помощью checklinks
$ checklinks http://www.404-error-page.com/
No errors encountered. Checked 1 pages at http://www.404-error-page.com/
Как видите, все в порядке. А если проверить сайт немного большего размера? В листинге 9.3 показано, что мог бы вывести сценарий checklinks в результате проверки сайта, содержащего недействительные ссылки.
Листинг 9.3. Проверка недействительных ссылок с помощью checklinks на более крупном веб-сайте
$ checklinks http://www.intuitive.com/library/
5 errors encountered. Checked 62 pages at http://intuitive.com/library/:
index/ in BeingEarnest.shtml
Archive/f8 in Archive/ArtofWriting.html
Archive/f11 in Archive/ArtofWriting.html
Archive/f16 in Archive/ArtofWriting.html
Archive/f18 in Archive/ArtofWriting.html
A copy of this output has been saved in intuitive.com.errors
Как показывают результаты, файл BeingEarnest.shtml содержит недействительную ссылку на /index/, потому что нет такого файла /index/. Также в файле ArtofWriting.html найдено четыре недействительные ссылки, имеющие странный вид.
Наконец, в листинге 9.4 показаны результаты проверки блога Дейва с обзорами фильмов, которая выявила в нем скрытые ошибки.
Листинг 9.4. Запуск сценария checklinks под управлением утилиты time, чтобы узнать продолжительность его работы
$ time checklinks http://www.daveonfilm.com/
No errors encountered. Checked 982 pages at http://www.daveonfilm.com/
real 50m15.069s
user 0m42.324s
sys 0m6.801s
Обратите внимание: добавив команду time перед другой командой, выполняющейся длительное время, можно узнать, как долго она выполнялась. В данном случае видно, что проверка всех 982 страниц в блоге http://www.daveonfilm.com/ потребовала 50 минут реального времени, из которых фактическая обработка заняла 42 секунды. Это очень много!
Файл с данными traverse.dat содержит список всех встреченных URL, а файл reject.dat — список всех встреченных, но непроверенных URL, обычно потому, что они являются внешними ссылками. Их проверкой мы займемся в следующем сценарии. Фактически найденные ошибки фиксируются в файле traverse.errors, как можно догадаться по строке в листинге 9.1.
Чтобы заставить этот сценарий сообщать о недействительных ссылках на изображения, добавьте команду grep для поиска в файле traverse.errors расширений имен файлов .gif, .jpeg или .png перед передачей результатов команде sed (которая здесь просто убирает из вывода все лишнее, чтобы сделать его более удобочитаемым).
Этот сценарий (листинг 9.5) является сопутствующим для сценария № 69 и основывается на результатах, произведенных им, выявляя все внешние ссылки на сайте или в его подкаталогах, обращение к которым приводит к ошибке «404 Not Found». Для простоты предполагается, что непосредственно перед данным сценарием выполнялся предыдущий сценарий и в текущем каталоге хранится файл *.rejects со списком URL.
Листинг 9.5. Сценарий checkexternal
#!/bin/bash
# checkexternal -- проверяет все ссылки на веб-сайте и конструирует список
# внешних ссылок, затем проверяет каждую, чтобы выявить среди них
# недействительные. Флаг -a заставляет сценарий вывести все ссылки,
# независимо от их доступности или недоступности; по умолчанию выводятся
# только недоступные ссылки.
listall=0; errors=0; checked=0
if [ "$1" = "-a" ] ; then
listall=1; shift
fi
if [ -z "$1" ] ; then
echo "Usage: $(basename $0) [-a] URL" >&2
exit 1
fi
trap "$(which rm) -f traverse*.errors reject*.dat traverse*.dat" 0
outfile="$(echo "$1" | cut -d/ -f3).errors.ext"
URLlist="$(echo $1 | cut -d/ -f3 | sed 's/www\.//').rejects"
rm -f $outfile # Подготовиться к выводу новой информации.
if [ ! -e "$URLlist" ] ; then
echo "File $URLlist not found. Please run checklinks first." >&2
exit 1
fi
if [ ! -s "$URLlist" ] ; then
echo "There don't appear to be any external links ($URLlist is empty)." >&2
exit 1
fi
#### Теперь все готово к анализу...
for URL in $(cat $URLlist | sort | uniq)
do
curl -s "$URL" > /dev/null 2>&1; return=$?
if [ $return -eq 0 ] ; then
if [ $listall -eq 1 ] ; then
echo "$URL is fine."
fi
else
echo "$URL fails with error code $return"
errors=$(( $errors + 1 ))
fi
checked=$(( $checked + 1 ))
done
echo ""
echo "Done. Checked $checked URLs and found $errors errors."
exit 0
Это не самый элегантный сценарий в книге. Он реализует метод простого перебора для проверки внешних ссылок. В блоке кода в для каждой найденной внешней ссылки вызывается команда curl, которая проверяет ее доступность, пытаясь получить содержимое по адресу URL ссылки и сразу отбрасывая его по получении.
Конструкция 2>&1 заслуживает отдельного упоминания: она перенаправляет выходное устройство с дескриптором 2 в выходное устройство с дескриптором 1. В командной строке выходное устройство с дескриптором 2 соответствует stderr (стандартному потоку вывода сообщений об ошибках), а выходное устройство с дескриптором 1 соответствует stdout (стандартному потоку вывода). Все, что выводится в stderr, конструкция 2>&1 перенаправляет в stdout. Но обратите внимание, что сначала поток stdout перенаправляется в /dev/null. Это виртуальное устройство, куда можно записать бесконечный объем данных, — своеобразная черная дыра в системе. То есть указанная конструкция гарантирует, что stderr так же будет перенаправлен в /dev/null. Мы выбрасываем информацию, потому что нас интересует только нулевой или ненулевой код, возвращаемый командой. Ноль сообщает об успехе; ненулевое значение — об ошибке.
Количество проверенных внутренних страниц определяется количеством строк в файле traverse.dat, а число внешних ссылок можно найти в файле reject.dat. Если указан флаг -a, сценарий выводит все внешние ссылки, независимо от их доступности или недоступности. В противном случае отображаются адреса URL только из недоступных ссылок.
Чтобы запустить сценарий, просто передайте ему в аргументе URL сайта для проверки.
Проверим сайт http://intuitive.com/ на наличие недействительных ссылок, как показано в листинге 9.6.
Листинг 9.6. Запуск сценария checkexternal для проверки http://intuitive.com/
$ checkexternal -a http://intuitive.com/
http://chemgod.slip.umd.edu/~kidwell/weather.html fails with error code 6
http://epoch.oreilly.com/shop/cart.asp fails with error code 7
http://ezone.org:1080/ez/ fails with error code 7
http://fx.crewtags.com/blog/ fails with error code 6
http://linc.homeunix.org:8080/reviews/wicked.html fails with error code 6
http://links.browser.org/ fails with error code 6
http://nell.boulder.lib.co.us/ fails with error code 6
http://rpms.arvin.dk/slocate/ fails with error code 6
http://rss.intuitive.com/ fails with error code 6
http://techweb.cmp.com/cw/webcommerce fails with error code 6
http://tenbrooks11.lanminds.com/ fails with error code 6
http://www.101publicrelations.com/blog/ fails with error code 6
http://www.badlink/somewhere.html fails with error code 6
http://www.bloghop.com/ fails with error code 6
http://www.bloghop.com/ratemyblog.htm fails with error code 6
http://www.blogphiles.com/webring.shtml fails with error code 56
http://www.blogstreet.com/blogsqlbin/home.cgi fails with error code 56
http://www.builder.cnet.com/ fails with error code 6
http://www.buzz.builder.com/ fails with error code 6
http://www.chem.emory.edu/html/html.html fails with error code 6
http://www.cogsci.princeton.edu/~wn/ fails with error code 6
http://www.ourecopass.org/ fails with error code 6
http://www.portfolio.intuitive.com/portfolio/ fails with error code 6
Done. Checked 156 URLs and found 23 errors.
Похоже, пришло время немного прибраться!
Одна из необычных возможностей веб-сервера Apache — встроенная поддержка защиты каталогов паролями, даже на общедоступном сервере. Это отличный способ ограничить доступ к закрытой информации на вашем веб-сайте, будь то платная служба или просто личный фотоальбом, предназначенный только для членов семьи.
Стандартные конфигурации требуют наличия в защищенном каталоге файла с именем .htaccess. Этот файл определяет название «зоны» безопасности и, что более важно, ссылается на отдельный файл, содержащий пары из имени учетной записи и пароля, которые используются для проверки права доступа к каталогу. Управление упомянутым файлом не вызывает проблем, за исключением того, что в составе Apache для этой цели имеется единственный инструмент — простенькая программа htpasswd, которая запускается из командной строки. Другой вариант — описываемый здесь сценарий apm, один из самых сложных сценариев в книге, — инструмент управления паролями, который можно запускать в браузере как CGI-сценарий и с его помощью добавлять новые учетные записи, изменять пароли существующих и удалять учетные записи из списка доступа.
Прежде всего, для управления доступом к каталогу необходимо иметь в нем правильно сформированный файл .htaccess. Для примера допустим, что этот файл содержит следующие строки:
$ cat .htaccess
AuthUserFile /usr/lib/cgi-bin/.htpasswd
AuthGroupFile /dev/null
AuthName "Members Only Data Area."
AuthType Basic
<Limit GET>
require valid-user
</Limit>
Имена учетных записей и пароли хранятся в отдельном файле .htpasswd. Если он отсутствует, его нужно создать. Вполне подойдет пустой файл: выполните команду touch .htpasswd и убедитесь, что созданный файл доступен для записи пользователю, с идентификатором которого запускается сам веб-сервер Apache (это может быть пользователь nobody). Теперь самое время переходить к сценарию в листинге 9.7. Однако он требует подготовки CGI-окружения, как описано в разделе «Запуск сценариев из этой главы» (глава 8). Сохраните сценарий в своем каталоге cgi-bin.
Листинг 9.7. Сценарий apm
#!/bin/bash
# apm -- Apache Password Manager (диспетчер паролей Apache) позволяет
# администратору легко добавлять, изменять или удалять учетные записи
# и пароли для доступа к подкаталогам в типичной конфигурации Apache
# (когда конфигурационный файл имеет имя .htaccess).
echo "Content-type: text/html"
echo ""
echo "<html><title>Apache Password Manager Utility</title><body>"
basedir=$(pwd)
myname="$(basename $0)"
footer="$basedir/apm-footer.html"
htaccess="$basedir/.htaccess"
htpasswd="$(which htpasswd) -b"
# Настоятельно рекомендуется включить следующий код для безопасности:
#
# if [ "$REMOTE_USER" != "admin" -a -s $htpasswd ] ; then
# echo "Error: You must be user <b>admin</b> to use APM."
# exit 0
# fi
# Получить имя файла с паролями из файла .htaccess
if [ ! -r "$htaccess" ] ; then
echo "Error: cannot read $htaccess file."
exit 1
fi
passwdfile="$(grep "AuthUserFile" $htaccess | cut -d\ -f2)"
if [ ! -r $passwdfile ] ; then
echo "Error: can't read password file: can't make updates."
exit 1
elif [ ! -w $passwdfile ] ; then
echo "Error: can't write to password file: can't update."
exit 1
fi
echo "<center><h1 style='background:#ccf;border-radius:3px;border:1px solid
#99c;padding:3px;'>"
echo "Apache Password Manager</h1>"
action="$(echo $QUERY_STRING | cut -c3)"
user="$(echo $QUERY_STRING|cut -d\& -f2|cut -d= -f2|\
tr '[:upper:]' '[:lower:]')"
case "$action" in
A ) echo "<h3>Adding New User <u>$user</u></h3>"
if [ ! -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user <b>$user</b> already appears in the file."
else
pass="$(echo $QUERY_STRING|cut -d\& -f3|cut -d= -f2)"
if [ ! -z "$(echo $pass|tr -d '[[:upper:][:lower:][:digit:]]')" ];
then
echo "Error: passwords can only contain a-z A-Z 0-9 ($pass)"
else
$htpasswd $passwdfile "$user" "$pass"
echo "Added!<br>"
fi
fi
;;
U ) echo "<h3>Updating Password for user <u>$user</u></h3>"
if [ -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user <b>$user</b> isn't in the password file?"
echo "searched for "^${user}:" in $passwdfile"
else
pass="$(echo $QUERY_STRING|cut -d\& -f3|cut -d= -f2)"
if [ ! -z "$(echo $pass|tr -d '[[:upper:][:lower:][:digit:]]')" ];
then
echo "Error: passwords can only contain a-z A-Z 0-9 ($pass)"
else
grep -vE "^${user}:" $passwdfile | tee $passwdfile > /dev/null
$htpasswd $passwdfile "$user" "$pass"
echo "Updated!<br>"
fi
fi
;;
D ) echo "<h3>Deleting User <u>$user</u></h3>"
if [ -z "$(grep -E "^${user}:" $passwdfile)" ] ; then
echo "Error: user <b>$user</b> isn't in the password file?"
elif [ "$user" = "admin" ] ; then
echo "Error: you can't delete the 'admin' account."
else
grep -vE "^${user}:" $passwdfile | tee $passwdfile >/dev/null
echo "Deleted!<br>"
fi
;;
esac
# Всегда перечислять текущих пользователей в файле паролей...
echo "<br><br><table border='1' cellspacing='0' width='80%' cellpadding='3'>"
echo "<tr bgcolor='#cccccc'><th colspan='3'>List "
echo "of all current users</td></tr>"
oldIFS=$IFS ; IFS=":" # Изменить разделитель слов...
while read acct pw ; do
echo "<tr><th>$acct</th><td align=center><a href=\"$myname?a=D&u=$acct\">"
echo "[delete]</a></td></tr>"
done < $passwdfile
echo "</table>"
IFS=$oldIFS # ...и восстановить его.
# Собрать строку выбора со всеми учетными записями...
optionstring="$(cut -d: -f1 $passwdfile | sed 's/^/<option>/'|tr '\n' ' ')"
if [ ! -r $footer ] ; then
echo "Warning: can't read $footer"
else
# ...и вывести нижний колонтитул.
sed -e "s/--myname--/$myname/g" -e "s/--options--/$optionstring/g" < $footer
fi
exit 0
Для нормальной работы этого сценария требуется очень многое. Необходимо правильно настроить не только конфигурацию веб-сервера Apache (или эквивалентного ему), но и содержимое файла .htaccess, и в файле .htpasswd должна иметься хотя бы запись для пользователя admin.
Сам сценарий извлекает в htpasswd имя файла с паролями из файла .htaccess и выполняет разные проверки, чтобы исключить наиболее типичные ошибки при работе с htpasswd, в том числе и ошибку недоступности файла для записи. Все это делает инструкция case перед основным блоком сценария.
Инструкция case определяет, какая из трех возможных операций запрошена — A (добавить пользователя), U (изменить запись с информацией о пользователе) или D (удалить пользователя), — и выполняет соответствующий фрагмент кода. Код операции и имя пользователя хранятся в переменной QUERY_STRING. Значение для этой переменной посылается на сервер веб-браузером в составе URL, в виде a=X&u=Y, где X — буквенный код операции, а Y — имя пользователя. Когда запрашивается операция изменения пароля или добавления пользователя, должен передаваться третий аргумент, p, с паролем.
Например, допустим, что мы добавляем нового пользователя joe с паролем knife. В результате этого действия веб-сервер передаст сценарию следующее значение в переменной QUERY_STRING:
a=A&u=joe&p=knife
Сценарий развернет эту строку, запишет в переменную action символ A, в переменную user имя joe и в переменную pass строку knife. Затем убедится, в строке , что пароль содержит только допустимые алфавитные символы.
В заключение, если все прошло успешно, будет вызвана программа htpasswd, чтобы зашифровать пароль и добавить его в файл .htpasswd . Также этот сценарий создает HTML-таблицу, в которой перечисляются все пользователи из .htpasswd вместе со ссылками [delete].
После вывода трех строк с заголовком HTML-таблицы сценарий продолжает выполнение со строки . Этот цикл while читает пары имя/пароль из файла .htpasswd, используя трюк с изменением разделителя входных полей (Input Field Separator, IFS) на двоеточие и восстановлением по завершении.
Сценарий полагается на присутствие HTML-файла с именем apm-footer.html, содержащего строки --myname-- и --options-- , которые в процессе вывода файла в stdout замещаются текущим именем CGI-сценария и списком пользователей соответственно.
Переменная $myname определяется механизмом CGI, который сохраняет в ней фактическое имя сценария. Сам сценарий конструирует переменную $optionstring из пар имя/пароль, хранящихся в файле .htpasswd .
HTML-файл с нижним колонтитулом, представленный в листинге 9.8, дает возможность выполнить операцию добавления пользователя, изменить пароль и удалить пользователя.
Листинг 9.8. Файл apm-footer.html добавляющий раздел с полями ввода для выполнения операций
<!-- нижний колонтитул с информацией для системы APM. -->
<div style='margin-top: 10px;'>
<table border='1' cellpadding='2' cellspacing='0' width="80%"
style="border:2px solid #666;border-radius:5px;" >
<tr><th colspan='4' bgcolor='#cccccc'>Password Manager Actions</th></tr>
<tr><td>
<form method="get" action="--myname--">
<table border='0'>
<tr><td><input type='hidden' name="a" value="A">
add user:</td><td><input type='text' name='u' size='15'>
</td></tr><tr><td>
password: </td><td> <input type='text' name='p' size='15'>
</td></tr><tr><td colspan="2" align="center">
<input type='submit' value='add' style="background-color:#ccf;">
</td></tr>
</table></form>
</td><td>
<form method="get" action="--myname--">
<table border='0'>
<tr><td><input type='hidden' name="a" value="U">
update</td><td><select name='u'>--options--</select>
</td></tr><tr><td>
password: </td><td><input type='text' name='p' size='10'>
</td></tr><tr><td colspan="2" align="center">
<input type='submit' value='update' style="background-color:#ccf;">
</td></tr>
</table></form>
</td><td>
<form method="get" action="--myname--"><input type='hidden'
name="a" value="D">delete <select name='u'> --options-- </select>
<br /><br /><center>
<input type='submit' value='delete' style="background-color:#ccf;"></
center></form>
</td></tr>
</table>
</div>
<h5 style='background:#ccf;border-radius:3px;border:1px solid
#99c;padding:3px;'>
From the book <a href="http://www.intuitive.com/wicked/">Wicked Cool Shell
Scripts</a>
</h5>
</body></html>
Вы почти наверняка пожелаете сохранить сценарий в том же каталоге, который требуется защитить паролем, однако можно предпочесть и каталог cgi-bin, как это сделали мы. В любом случае убедитесь, что переменные htpasswd и basedir получают правильные значения в начале сценария. Вам также понадобится файл .htaccess, определяющий права доступа, и файл .htpasswd, доступный для записи пользователю, с привилегиями которого выполняется веб-сервер Apache в вашей системе.
ПРИМЕЧАНИЕ
Перед запуском сценария apm в первую очередь создайте учетную запись admin, чтобы можно было использовать его в последующих вызовах! В коде предусмотрена специальная проверка, которая позволит создать учетную запись admin, если файл .htpasswd пуст.
Результат работы сценария apm показан на рис. 9.1. Обратите внимание, что он не только перечисляет все учетные записи со ссылкой для удаления, но
Рис. 9.1. Система управления паролями в Apache на основе сценария командной оболочки
также предоставляет возможность создать новую учетную запись, изменить пароль существующей, перечислить все учетные записи или удалить любую из них.
Программа htpasswd, входящая в состав веб-сервера Apache, предлагает отличный интерфейс командной строки для добавления новой учетной записи и шифрования пароля перед сохранением в базе данных. Но только одна из двух распространенных версий htpasswd поддерживает работу в пакетном режиме и может использоваться в сценариях — то есть позволяет сценарию передавать в командной строке имя учетной записи и пароль. Узнать, какая версия установлена у вас, очень просто: если при попытке выполнить htpasswd с флагом -b программа не выведет сообщения об ошибке, значит, вам повезло и у вас установлена более современная версия. Впрочем, ваши шансы на успех очень велики.
Имейте в виду, что, если сценарий установлен неправильно, любой, кто узнает структуру URL, сможет добавить себя в файл доступа и удалить другого пользователя. Это плохо. Одно из решений состоит в том, чтобы позволить запускать сценарий только пользователю admin (упомянутому в закомментированном коде в начале сценария). Другой способ обезопасить сценарий — поместить его в каталог, который уже защищен паролем.
Хотя программа ftp все еще доступна в большинстве систем, она постепенно вытесняется более новыми протоколами передачи данных, такими как rsync и ssh (secure shell — защищенная командная оболочка). Это объясняется несколькими причинами. После выхода первого издания этой книги стали очевидны некоторые слабые стороны FTP, связанные с плохим масштабированием и слабой защищенностью. В новом мире «больших данных» популярность приобретают более эффективные протоколы. Кроме того, FTP осуществляет передачу данных в открытом виде, что обычно не вызывает проблем в домашних или корпоративных сетях, но только не в случаях, когда FTP используется для передачи данных в открытых сетях, например, при подключении через общественные точки доступа к Интернету в библиотеках или кофейнях, которыми пользуется масса народу.
Все современные серверы должны поддерживать более безопасный пакет ssh, обеспечивающий сквозное шифрование. Программа, осуществляющая передачу данных в зашифрованном виде, называется sftp, и хотя она еще более примитивная, чем ftp, мы все же можем пользоваться ею. В листинге 9.9 показано, как с помощью sftp организовать защищенную синхронизацию файлов.
ПРИМЕЧАНИЕ
Если в вашей системе отсутствует пакет ssh, пожалуйтесь своему поставщику или администраторам, потому что этому нет никакого оправдания. Если у вас имеются соответствующие привилегии, можете сами попробовать получить пакет на сайте http://www.openssh.com/ и установить его.
Листинг 9.9. Сценарий sftpsync
#!/bin/bash
# sftpsync -- принимая имя удаленного каталога на сервере sftp, выгружает
# все новые или изменившиеся файлы в удаленную систему. Для синхронизации
# использует файл с отметкой времени и удивительно изобретательно
# подобранным именем .timestamp.
timestamp=".timestamp"
tempfile="/tmp/sftpsync.$$"
count=0
trap "$(which rm) -f $tempfile" 0 1 15 # Удалить временный файл по завершении.
if [ $# -eq 0 ] ; then
echo "Usage: $0 user@host { remotedir }" >&2
exit 1
fi
user="$(echo $1 | cut -d@ -f1)"
server="$(echo $1 | cut -d@ -f2)"
if [ $# -gt 1 ] ; then
echo "cd $2" >> $tempfile
fi
if [ ! -f $timestamp ] ; then
# Если файл с отметкой времени отсутствует, выгрузить все файлы.
for filename in *
do
if [ -f "$filename" ] ; then
echo "put -P \"$filename\"" >> $tempfile
count=$(( $count + 1 ))
fi
done
else
for filename in $(find . -newer $timestamp -type f -print)
do
echo "put -P \"$filename\"" >> $tempfile
count=$(( $count + 1 ))
done
fi
if [ $count -eq 0 ] ; then
echo "$0: No files require uploading to $server" >&2
exit 1
fi
echo "quit" >> $tempfile
echo "Synchronizing: Found $count files in local folder to upload."
if ! sftp -b $tempfile "$user@$server" ; then
echo "Done. All files synchronized up with $server"
touch $timestamp
fi
exit 0
Программа sftp позволяет передать ей последовательность команд через конвейер или стандартный ввод, что делает сценарий довольно простым: основная его часть связана с конструированием последовательности команд для выгрузки всех изменившихся файлов. В самом конце эта конструкция передается программе sftp для выполнения.
Если ваша версия sftp не возвращает ненулевой код в случае неудачной попытки передать файлы, просто удалите условный блок в конце сценария и замените его следующими командами:
sftp -b $tempfile "$user@$server"
touch $timestamp
Так как sftp требует передачи учетных данных в формате user@host, данный сценарий получился даже проще, чем эквивалентный сценарий, использующий FTP. Обратите также внимание на флаг -P в командах put: он требует от удаленного сервера сохранить локальные права доступа к файлам, а также время их создания и последнего изменения.
Перейдите в каталог с исходными файлами, проверьте существование целевого каталога и запустите сценарий, передав ему свое имя пользователя, имя сервера и имя удаленного каталога. Для простых случаев можно создать псевдоним с именем ssync (source sync — «синхронизировать исходные файлы»), который будет выполнять синхронизацию определенного каталога, автоматически вызывая сценарий sftpsync:
alias ssync="sftpsync [email protected] /wicked/scripts"
Запуск сценария sftpsync с именем пользователя, сервера и каталога в аргументах командной строки выполнит синхронизацию ваших каталогов, как показано в листинге 9.10.
Листинг 9.10. Запуск сценария sftpsync
$ sftpsync [email protected] /wicked/scripts
Synchronizing: Found 2 files in local folder to upload.
Connecting to intuitive.com...
[email protected]'s password:
sftp> cd /wicked/scripts
sftp> put -P "./003-normdate.sh"
Uploading ./003-normdate.sh to /usr/home/taylor/usr/local/etc/httpd/htdocs/
intuitive/wicked/scripts/003-normdate.sh
sftp> put -P "./004-nicenumber.sh"
Uploading ./004-nicenumber.sh to /usr/home/taylor/usr/local/etc/httpd/htdocs/
intuitive/wicked/scripts/004-nicenumber.sh
sftp> quit
Done. All files synchronized up with intuitive.com
Сценарий-обертка, вызывающий sftpsync, оказался чрезвычайно полезным. Мы использовали его на всем протяжении работы над этой книгой для синхронизации копий сценариев в веб-архиве http://www.intuitive.com/wicked/ с версиями, хранящимися на наших собственных серверах, без привлечения небезопасного протокола FTP.
Этот сценарий-обертка ssync, представленный в листинге 9.11, содержит всю необходимую логику для копирования локального каталога (переменная localsource) и создания файла архива, так называемого тарболла (по имени команды tar, используемой для его создания) с последними версиями всех файлов.
Листинг 9.11. Сценарий-обертка ssync
#!/bin/bash
# ssync -- Если что-то изменилось, создает тарболл и копирует его
# в удаленный каталог с помощью sftp, используя sftpsync.
sftpacct="[email protected]"
tarballname="AllFiles.tgz"
localsource="$HOME/Desktop/Wicked Cool Scripts/scripts"
remotedir="/wicked/scripts"
timestamp=".timestamp"
count=0
# Прежде всего проверить наличие локального каталога и файлов в нем.
if [ ! -d "$localsource" ] ; then
echo "$0: Error: directory $localsource doesn't exist?" >&2
exit 1
fi
cd "$localsource"
# Проверить: изменились ли какие-нибудь файлы.
if [ ! -f $timestamp ] ; then
for filename in *
do
if [ -f "$filename" ] ; then
count=$(( $count + 1 ))
fi
done
else
count=$(find . -newer $timestamp -type f -print | wc -l)
fi
if [ $count -eq 0 ] ; then
echo "$(basename $0): No files found in $localsource to sync with remote."
exit 0
fi
echo "Making tarball archive file for upload"
tar -czf $tarballname ./*
# Готово! Теперь передадим управление сценарию sftpsync.
exec sftpsync $sftpacct $remotedir
Если синхронизация необходима, создается новый файл архива, и все файлы (включая новый архив, конечно же) выгружаются на сервер, как показано в листинге 9.12.
Листинг 9.12. Запуск сценария ssync
$ ssync
Making tarball archive file for upload
Synchronizing: Found 2 files in local folder to upload.
Connecting to intuitive.com...
[email protected]’s password:
sftp> cd shellhacks/scripts
sftp> put -P "./AllFiles.tgz"
Uploading ./AllFiles.tgz to shellhacks/scripts/AllFiles.tgz
sftp> put -P "./ssync"
Uploading ./ssync to shellhacks/scripts/ssync
sftp> quit
Done. All files synchronized up with intuitive.com
Одним из дальнейших усовершенствований мог бы стать вызов ssync из cron каждые несколько часов в рабочие дни, workday, чтобы резервное копирование локальных файлов осуществлялось незаметно и без участия человека.