Книга: Сценарии командной оболочки. Linux, OS X и Unix. 2-е издание
Назад: Глава 8. Инструменты веб-мастера
Дальше: Глава 10. Администрирование интернет-сервера

Глава 9. Администрирование веб-сервера

Если вы занимаетесь поддержкой веб-сервера или отвечаете за работу веб-сайта, простого или сложного, то наверняка регулярно решаете какие-то повторяющиеся задачи, такие как выявление недействительных внутренних или внешних ссылок. Многие из этих задач можно автоматизировать с использованием сценариев командной оболочки. То же касается некоторых типичных клиент/серверных задач, таких как управление доступом к информации в каталогах веб-сервера с использованием паролей.

№ 69. Выявление недействительных внутренних ссылок

Несколько сценариев в главе 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 (которая здесь просто убирает из вывода все лишнее, чтобы сделать его более удобочитаемым).

№ 70. Выявление недействительных внешних ссылок

Этот сценарий (листинг 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.

Похоже, пришло время немного прибраться!

№ 71. Управление паролями в Apache

Одна из необычных возможностей веб-сервера 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 &quot;^${user}:&quot; 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 перед основным блоком сценария.

Операции с файлом .htpasswd

Инструкция 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. Обратите внимание, что он не только перечисляет все учетные записи со ссылкой для удаления, но

09-01.tif

Рис. 9.1. Система управления паролями в Apache на основе сценария командной оболочки

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

Усовершенствование сценария

Программа htpasswd, входящая в состав веб-сервера Apache, предлагает отличный интерфейс командной строки для добавления новой учетной записи и шифрования пароля перед сохранением в базе данных. Но только одна из двух распространенных версий htpasswd поддерживает работу в пакетном режиме и может использоваться в сценариях — то есть позволяет сценарию передавать в командной строке имя учетной записи и пароль. Узнать, какая версия установлена у вас, очень просто: если при попытке выполнить htpasswd с флагом -b программа не выведет сообщения об ошибке, значит, вам повезло и у вас установлена более современная версия. Впрочем, ваши шансы на успех очень велики.

Имейте в виду, что, если сценарий установлен неправильно, любой, кто узнает структуру URL, сможет добавить себя в файл доступа и удалить другого пользователя. Это плохо. Одно из решений состоит в том, чтобы позволить запускать сценарий только пользователю admin (упомянутому в закомментированном коде в начале сценария). Другой способ обезопасить сценарий — поместить его в каталог, который уже защищен паролем.

№ 72. Синхронизация файлов с помощью SFTP

Хотя программа 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, чтобы резервное копирование локальных файлов осуществлялось незаметно и без участия человека.

Назад: Глава 8. Инструменты веб-мастера
Дальше: Глава 10. Администрирование интернет-сервера