MCP

среда, 28 октября 2009 г.

Использование DefaultHttpHandler для отдачи файлов пользователю

Тут на конкурсе задали вопрос: чем отличаются Http Handlers и Http Modules. Ответов полно, дублировать их желания нет.  Всегда можно открыть MSDN и почитать про это.

Вкратце, Handler, это обработчик запроса, например, когда обращаемся на /default.aspx, в web.config ищется подходящий обработчик и ему запрос отдаётся на растерзание. Естественно, если вы поглядите в web.config для вашего приложения, вы там скорее всего такого не найдёте. Но если вспомнить иеархию конфигруационных файлов в .NET, то становится ясно, что нужно искать в базовом web.config файле. У меня он находится тут: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\web.config.

Этот зверь в 30Кб, полностью описывает стандартные настройки приложения, которые можно заменить в своём. Там как раз и есть обработчик aspx-файлов:

<add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="true">

Собственно он и говорит рабочему процессу, что все подобные запросы должна обрабатывать фабрика с названием PageHandler (фабрика, это небольшой класс, который позволяет уже самостоятельно выбрать нужный Handler, в зависимости от более тонких условий). Также там можно найти другие интересные строчки:

<add path="*.config" verb="*" type="System.Web.HttpForbiddenHandler" validate="true"/>
...
<add path="*.soap" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="false"/>

Например, эти говорят что *.config файлы нельзя показывать (например тот же web.config), а *.soap нужно для Remoting. Естественно, все эти правила можно заменить в своём приложении, да и при этом не забыть, что сам IIS по умолчанию не все расширения отдаёт ASP.NET, и надо добавить соответствующий фильтр в  настройках вашего приложения (ну или глобальный, на все файлы).

Про Http Handler's почему-то не получилось коротко, но это я случайно.   Дальше я расскажу более интересную вещь. А пока немножко про Http Modules.

Собственно, с ними всё просто, поскольку файл мы уже открыли и видим что за httpHandlers идут httpModules. Они-то  и будут будут вызваны HttpApplication во время обработки запроса, при этом они могут сделать что-нибудь полезное не прерывая и не перенаправляя запрос. Например, разобраться с сессией, авторизацией или логгированием. Да мало ли чем, всегда их можно подключить и отключить. Например мы хотим фатальные ошибки приложения сразу отправлять админу по ICQ. Сделали модуль, подключили к нужным приложениям. Всё работает, красота.  


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

Я часто встречал реализацию в виде /getfile.ashx?id=123 — где собственный хендлер как-то пытается отдать файлы. Идея хорошая, но не очень красивая, к тому же приходится рисовать собственный хендлер, отдающий файлы, что как-то неправильно.

Небольшое отсупление, по возможности старайтесь передавать имя файла в запросе, а не через Content-Disposition, иначе будут  проблемы с русскими именами файлов, да и периодические глюки, когда у клиента имя какое-то не такое. Для этого всего-лишь нужно делать запросы вида /getfile.ashx?id=123/requiredFileName немного подправить разбор входящих параметров, а дальше браузер сам придумает как обозвать файл.

Собственно моя идея состоит в том, что нужно использовать существующий DefaultHttpHandler, который и предназначен для отдачи статики, только для этого придётся выполнить пару простых условий:

  • Необходим .NET Framework 3.5 SP1, потому что в предыдущих реализация данного хендлера более примитивная.
  • Файлы должны быть физически доступны из приложения, т.е. пользователь должен иметь теоретическую возможность их скачать (правда имя может быть любое и лежать они могут как хотят. Для запрета прямой скачки, делаем соответствующие правила в web.config)

Теперь, собственно, осталось придумать по какому пути пользователи будут обращаться к файлам и навесить на этот путь свой простенький хендлер, отнаследованный от DefaultHttpHandler. Можете использовать различные url-rewriting решения, или написать своё. Если вам приходится вешаться на aspx файлы, то можете в фабрике возвращать следующий Handler:

PageParser.GetCompiledPageInstance(url, null, context)

Т.е. практически вручную запустить обработку страницы (с некоторым оговорками, но не о них сейчас речь).

Далее, в своём хендлере делаем следующее:

  • В методе BeginRequest  определяем "настоящий" путь к файлу по запросу. Например, запрос, /123/myfile.jpg меняем на /files/storedFile_123.dat
  • вызываем у контекста (вам его передадут) метод: context.RewritePath с полученным путём.
  • Дальше, чтобы не отдавать запрос IIS (а для оптимизации DefaultHttphandler попробует это сделать), что-нибудь пишем в Response, например context.Response.Write(' '). 
  • Вызываем базовый метод BeginRequest

В общем-то всё. Мы подменили путь и сказали обработать его как следует. С докачкой, и прочими плюшками. Но небольшое уточнение, зачем же не стоит отдавать запрос IISу? На самом деле, в определённых случаях это нужно делать, ибо производительность будет выше, но IIS делает пару нехороший действий:

  • Записывает в заголовки Content-Location с настоящим именем файла, т.е. хоть и не страшно, но мы светим то, как у нас хранятся файлы, т.е. неаккуратненько как-то
  • Если он не знает расширения файла, то в целях безопасности может его и не отдать (Свойства сервера, типы MIME — тут указаны расширения, которые IIS знает)

Ещё можно обрабатывать ошибки, писать логи и статистику, но это уже тонкости и детали, главное, что достаточно быстро (гораздо быстрее чем читать этот пост ).


Вышесказанное относится к IIS 6.0, в седьмом, возможно всё будет ещё проще, но я с ним мало работал, и в основном в классическом режиме, так что не буду ничего утверждать заранее.

2 комментария: