Книга: Написание скриптов для Blender 2.49

Разработка сервера  CAPTCHA

Разработка сервера  CAPTCHA

При использовании модулей, доступных в полном дистрибутиве Питона, задача создания сервера HTTP становится не такой уж пугающей, как может показаться. Наш сервер Captcha будет основан на классах, предоставленных модулем Питона BaseHTTPServer, так что мы начинаем с импорта этого модуля вместе с несколькими дополнительными модулями-утилитами:

import BaseHTTPServer
import re
import os
import shutil

Модуль BaseHTTPServer определяет два класса, которые вместе включают полную реализацию сервера HTTP. Класс BaseHTTPServer реализует основной сервер, который будет слушать поступающие HTTP-запросы на некотором сетевом порту, и мы используем этот класс, как есть.

При получении корректного HTTP-запроса BaseHTTPServer пошлет этот запрос обработчику запросов. Наша реализация такого обработчика запросов, основанная на BaseHTTPRequestHandler, довольно скудна, так как ожидается, что всё, что он будет делать - запрашивать поля GET и HEAD в форме captcha?text=abcd. Следовательно, всё мы должны сделать - переписать методы do_GET() и do_HEAD() базового класса.

От запроса HEAD ожидается возвращение только заголовков запрошенного объекта, а не содержимого, чтобы сохранять время, за которое содержимое не изменится со времени последнего запроса (что-то, что может быть определено проверкой заголовка Last-Modified). Мы игнорируем такую аккуратность; мы возвращаем заголовки именно тогда, когда мы получаем запрос HEAD, но мы, тем не менее, будем генерировать полностью новое изображение. Это в некоторой степени расточительно, но зато код будет простым. Если важна производительность, можно разработать другую реализацию.

Наша реализация начинается с определения метода do_GET(), который просто вызывает метод do_HEAD(), который будет генерировать вопрос Captcha и возвращать заголовки клиенту. do_GET(), впоследствии, копирует содержание файлового объекта, возвращённого методом do_HEAD() в выходной файл, такой как объект обработчика запроса (выделено), который в свою очередь возвращает это содержимое клиенту (например, браузеру):

class CaptchaRequestHandler(
         BaseHTTPServer.BaseHTTPRequestHandler):
   def do_GET(self):
      f=self.do_HEAD()
      shutil.copyfileobj(f,self.wfile)
      f.close()

Метод do_HEAD() сначала определяет, получили ли мы правильный запрос (то есть, URI в форме captcha?text=abcd), вызывая метод gettext() (выделено, определяется позже в коде). Если URI некорректен, метод gettext(), возвращает None и тогда do_HEAD() возвращает клиенту ошибку File not found (Файл не найден), вызывая метод send_error() базового класса:

   def do_HEAD(self):
      text=self.gettext()
      if text==None:
            self.send_error(404, "File not found")
            return None

Если был запрошен корректный URI, фактическое изображение генерируется методом captcha(), который возвращает имя файла сгенерированного изображения. Если этот метод терпит неудачу по любой причине, клиенту возвращается Internal server error (Внутренняя ошибка сервера):

      try:
            filename = self.captcha(text)
      except:
            self.send_error(500, "Internal server error")
            return None

Если все прошло хорошо, мы открываем файл изображения, отсылаем клиенту ответ 200 (показывающий успешную операцию), и возвращаем заголовок Content-type, устанавливающий, что мы возвращаем png-изображение. Затем мы используем функцию fstat() с номером handle открытого файла в качестве аргумента, чтобы извлечь длину сгенерированного изображения и вернуть её как заголовок  Content-Length (выделено), сопроводив временем модификации и пустой строкой, означающей конец заголовков перед возвратом открытого файлового объекта f:

      f = open(filename,'rb')
      self.send_response(200)
      self.send_header("Content-type", 'image/png')
      fs = os.fstat(f.fileno())
      self.send_header("Content-Length", str(fs[6]))
      self.send_header("Last-Modified",
                self.date_time_string(fs.st_mtime))
      self.end_headers()
      return f

Метод gettext() проверяет, что запрос, передаваемый нашему обработчику запросов в переменной пути - правильный URI, сверяя его с регулярным выражением. Функция match() из модуля Питона re возвращает MatchObject (объект сопоставления), если регулярное выражение соответствует параметру, и None, если нет. Если есть соответствие, мы возвращаем содержание первой группы объекта сопоставления (символы, которые соответствуют выражению между круглыми скобками в регулярном выражении, в нашем случае значение текстового аргумента), в противном случае мы возвращаем None:

   def gettext(self):
      match = re.match(r'^.*/captcha?text=(.*)$',
                       self.path)
      if match != None:
         return match.group(1)
      return None

Теперь мы добрались до задачи, специфичной для Блендера - сгенерировать рендеренный в 3D текст, который будет возвращён в виде png изображения. Метод captcha() принимает текст для рендера как аргумент, и возвращает имя файла сгенерированного изображения. Мы допускаем, что освещение и камера в .blend файле, в котором мы запускаем captcha.py, настроены правильно, чтобы удобочитаемо отображать наш текст. Следовательно, метод captcha() просто настраивает правильным образом объект Text3d и рендерит его.

Первая задача состоит в том, чтобы определить текущую сцену и проверить, присутствует ли объект с именем Text, который можно использовать заново (выделено). Заметьте, что вполне допустимо иметь другие объекты на сцене, чтобы ещё более затемнить отображение:

   def captcha(self,text):
      import Blender
      scn = Blender.Scene.GetCurrent()
      text_ob = None
      for ob in scn.objects:
         if ob.name == 'Text' :
            text_ob = ob.getData()
            break

Если не нашлось никакого ранее используемого объекта Text3d, создаём новый :

      if text_ob == None:
         text_ob = Blender.Text3d.New('Text')
         ob=scn.objects.new(text_ob)
         ob.setName('Text')

Следующий шаг - установить текст объекта Text3d в значение аргумента, переданного в метод captcha(), и сделать его трёхмерным, настроив глубину выдавливания. Мы также изменяем ширину символов и сокращаем расстояние между ними, чтобы ухудшить разделение. Добавление небольшого скоса (bevel) смягчит контуры символов, что может добавить трудностей для робота, различающего символы, если настроено искусное освещение (выделено). Мы могли бы решить использовать другой шрифт для нашего текста, который ещё труднее для чтения ботом, и здесь как раз место для установки такого шрифта (смотри следующий информационный блок).


Чего-то не хватает

Документация API Блендера имеет небольшой пропуск: как будто не существует способа настроить другой шрифт для объекта Text3d. Тем не менее, есть недокументированный метод setFont(), который принимает объект Font в качестве аргумента. Код, выполняющий изменение шрифта должен выглядеть похожим на это:

fancyfont=Text3d.Load('/usr/share/fonts/ttf/myfont.ttf')
text_ob.setFont(fancyfont)

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

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

      text_ob.setText(text)
      text_ob.setExtrudeDepth(0.3)
      text_ob.setWidth(1.003)
      text_ob.setSpacing(0.8)
      text_ob.setExtrudeBevelDepth(0.01)
      ob.makeDisplayList()

Как только наш объект Text3d будет на месте, нашей следующей задачей станет отрендерить его изображение в файл. Сначала мы извлекаем контекст рендера из текущей сцены и устанавливаем displayMode в 0, чтобы предотвратить появление дополнительного окна рендера:

      context = scn.getRenderingContext()
      context.displayMode=0

Затем, мы устанавливаем размер изображения и указываем, что нам нужен формат png. Включением RGBA и установкой альфа-режима в 2 мы гарантируем, что там не будет видно никакого неба, и что наше изображение будет иметь хороший прозрачный фон:

      context.imageSizeX(160)
      context.imageSizeY(120)
      context.setImageType(Blender.Scene.Render.PNG)
      context.enableRGBAColor()
      context.alphaMode=2

Даже если мы рендерим простое неподвижное изображение, мы используем метод renderAnim() контекста рендера, поскольку иначе результаты рендерятся не в файл, а только в буфер. Следовательно, мы устанавливаем начальный и конечный кадры анимации в 1 (точно так же, как и текущий кадр), чтобы удостовериться, что мы генерируем простой одиночный кадр. Затем мы используем метод getFrameFilename(), чтобы получить имя файла (с полным путём) отрендеренного кадра (выделено). Далее мы одновременно сохраняем это имя файла и возвращаем его как результат:

      context.currentFrame(1)
      context.sFrame=1
      context.eFrame=1
      context.renderAnim()
      self.result=context.getFrameFilename()
      return self.result

Последняя часть скрипта определяет функцию run(), чтобы запустить сервер Captcha, и вызывает эту функцию, если скрипт выполняется автономно (то есть, если он не был импортирован как модуль). Определив функцию run() таким образом, мы можем изолировать часто используемые параметры сервера по умолчанию, как например, номер порта, который прослушивается (выделено), но допустимо повторное использование модуля, если потребовалась другая настройка:

def run(HandlerClass = CaptchaRequestHandler,
      ServerClass = BaseHTTPServer.HTTPServer,
      protocol="HTTP/1.1"):
      port = 8080
      server_address = ('', port)
      HandlerClass.protocol_version = protocol
      httpd = ServerClass(server_address, HandlerClass)
      httpd.serve_forever()
if __name__ == '__main__':
      run()

Полный код доступен как captcha.py в файле captcha.blend, и сервер можно запустить несколькими путями: из текстового редактора (с Alt + P), из меню Scripts | render | captcha, или запустив Блендер в фоновом режиме из командной строки. Чтобы остановить сервер снова, необходимо завершить Блендер. Обычно это можно сделать посредством нажатия Ctrl + C в консоли или в окне DOSbox.


Предупреждение

Заметьте, что этот сервер реагирует на чьи угодно запросы, а это далеко небезопасно. Как минимум он должен быть запущен через межсетевой экран, который ограничивает доступ к нему только для сервера, которому требуются вопросы Captcha. Прежде чем запускать его в любом месте, которое может быть доступно из Интернета, вы должны тщательно подумать о безопасности вашей сети!

Оглавление книги

Оглавление статьи/книги

Генерация: 1.158. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз