Libapreq2 + <SUP> + MSIE = Fail

| Нет комментариев

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

Оказалось, что это происходит только с MSIE, и именно с теми ссылками, которые находятся в блогах на livejournal.com. В строке запроса (QUERY_STRING) в таких ссылках обычно четыре параметра — два текстовых и два числовых:

http://whoyougle.ru/place/distance/?from=%D0%9F%D0%B0%D1%80%D0%B8%D0%B6&to=%D0%91%D0%B0%D1%80%D1%81%D0%B5%D0%BB%D0%BE%D0%BD%D0%B0&from_which=498817&to_which=1819729

Значения параметров from и to — то, что пользователь вводил в полях ввода, а from_which и to_which уточняют запрос, выбирая из нескольких одноименных названий одно конкретное. (В общем-то, несколько избыточно, и от первых двух параметров мы уже отказались, но суть не в этом.)

После перехода по ссылке в адресной строке вместо URI-escaped-последовательности оказывались русские буквы, и первое подозрение было — MSIE не очень хорошо работает с такими адресами и передает на сервер что-то искаженное.

MSIE, однако, хорош тем, что щелкает при загрузке страницы, и вместо одного щелчка при переходе по ссылке было два. Оказалось, что товарищи жжсты подменяли ссылку и превращали ее в ссылку на их бесконечно хитрый редиректер (который сначала загружал ту же страницу ЖЖ, а потом делал редирект):

http://community.livejournal.com/ru_travel/14519597.html?thread=168965421&dr_log=-1&linkout=http%3A//whoyougle.ru/place/distance/%3Ffrom%3D%25D0%259F%25D0%25B0%25D1%2580%25D0%25B8%25D0%25B6%26to%3D%25D0%2591%25D0%25B0%25D1%2580%25D1%2581%25D0%25B5%25D0%25BB%25D0%25BE%25D0%25BD%25D0%25B0%26from_which%3D498817%26to_which%3D1819729

После этого происходил переход на адрес, в котором вместо from=%D0%9F%D0%B0%D1%80%D0%B8%D0%B6 оказывалась неэкранированная последовательность байт, эквивалентная запросу from=%c1%e0%f0%f1%e5%eb%ee%ed%e0. А это уже не изначальный UTF-8, а почему-то голый Windows-1251.

Реальные же непонятки возникли, когда оказалось, что оставшиеся параметры from_which и to_which были вообще не видны на сервере. Но такого же не может быть! В переменной $ENV{QUERY_STRING} строка была вся и целиком.

Мы пользуемся библиотекой libapreq2 для разбора параметров, чтения кук и загрузки файлов. Perl-модули Apache2::Request и др. предоставляют интерфейс, аналогичный традиционному CGI.pm, и в нехохошем до сих пор замечены не были.

А тут в списке параметров, возвращаемых вызовом $req->param(), оказывались только те, которые стояли в строке запроса до первой пары со значением в кодировке Windows-1251.

Круто, лезем с Сергеем в код libapreq2 (предварительно обновившись до последней версии, но неадекватное поведение осталось).

Сказать, что я был удивлен, — ничего не сказать. Libapreq2 при разборе строки делает бешеное число действий для того, чтобы распознать кодировку запроса. Казалось бы, зачем она это делает, если Apache2::Request все равно в итоге отдает октеты?

Алгоритм выявления кодировки (см. файл util.c) такой:

  1. изначально ожидается ASCII. Если нет, то
  2. предполагаем UTF-8. Если нет, то
  3. предполагаем Latin-1. Если нет управляющих символов, то
  4. устанавливаем CP1252.

Да-да, Latin-1 и CP1252. (Сразу видно, что писано моноглотами.) Функция, выполняющая эти эвристики, кстати, называется apreq_charset_divine. Divine — англ. гадать, предсказывать, пророчествовать.

Первый байт строки %c1%e0%f0%f1%e5%eb%ee%ed%e0 содержит два единичных старших бита, и функция-пророк считала его началом UTF-последовательности. Правда, на втором же байте понимало, что это не так, и... и в итоге функция apreq_parse_query_string (см. файл param.c) завершала работу:

s = apreq_param_decode(&param, pool, start, nlen, vlen);
if (s != APR_SUCCESS)
    return s;

apreq_param_tainted_on(param);
apreq_value_table_add(&param->v, t);

Ни сама пара, в которой содержалась строка в кодировке Windows-1251, ни все последующие не попадали в таблицу параметров строки запроса.

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

s = apreq_param_decode(&param, pool, start, nlen, vlen);
if (s == APR_SUCCESS) {
    apreq_param_tainted_on(param);
    apreq_value_table_add(&param->v, t);
}

Собираем libapreq2, подменяем libapreq.so, и — вуаля! — видим значения всех параметров после тех, что проигнорировали.

P. S. В исходниках есть моноглотские определения типа

enum   apreq_charset_t { APREQ_CHARSET_ASCII = 0, APREQ_CHARSET_LATIN1 = 1, APREQ_CHARSET_CP1252 = 2, APREQ_CHARSET_UTF8 = 8 }

и самописаня функция apreq_cp1252_to_utf8, реализация которой напоминает мой код 2002 года :-)

Комментировать

Страницы

  • img

Об этой записи

Сообщение опубликовано 21.04.2010 22:55. Автор — ash.

Предыдущая запись — 1 яблоко. 2 яблока. 5 яблок. Или о миграции алгоритмов

Следующая запись — Perl из XSLT — 2

Смотрите новые записи на главной странице или загляните в архив, где есть ссылки на все сообщения.