![image](https://ic.pics.livejournal.com/egor_23/73280836/1707368/1707368_1000.jpg)
Сайт The Daily WTF уже 15 лет собирает курьёзные, дикие и/или печальные истории из мира ИТ. Я перевёл несколько рассказов, показавшихся мне интересными. Все имена и названия компаний изменены. Предыдущие выпуски можно найти по метке "любопытные извращения".
История первая. Конец света месяца
[Оригинал]
Если спросить инженера-разработчика, безопасно ли ходить по мосту, то он с удовольствием расскажет вам, насколько надёжны мосты, как в них работает математика, как далеко мы продвинулись в вопросах строительной безопасности. После разговора с ним у вас создастся впечатление, что ни один мост на Земле ни за что не развалится. Но если спросить у инженера-разработчика ПО о банках, то вы скорее всего будете в ужасе, и с вероятностью 50/50 убедите себя вложить все деньги в биткоин. Банки печально известны своими плохими решениями при создании ПО — не потому, что эти решения отвратительны, а потому, что большинство людей предполагает, что банки более аккуратны и внимательны к безопасности.
Като работает в Inibank, где в качестве ядра банковской системы используется коммерческий продукт под названием T24. Система T24 используется сотнями банков по всему миру. Её можно настраивать под широкий диапазон банковских решений. Как и в случае с большинством настраиваемых пакетов. существуют программисты, специализирующиеся в написании кода для него, и консультанты, помогающие банкам в выполнении крупных обновлений.
Персонал Inibank был загружен работой, поэтому банк пригласил для участия в особом проекте консультанта. В конце рабочего дня банк проводил процесс завершения работы, который необходим для того, чтобы все деньги попадали туда, куда направлялись, пересчитывались все необходимые выходные данные и выполнялись все требуемые отчёты. В банках этот процесс также меняет системную дату на ближайший рабочий день. Именно поэтому когда вы занимаетесь онлайн-банкингом в воскресенье, ни одна операция не начинает обрабатываться до утра понедельника. Консультант должен был создать новый отчёт, который бы выполнялся во время процесса завершения работы и включал в себя дополнительную обработку, если дата соответствовала концу месяца.
Като показал новому консультанту, как в банке настраиваются отчёты конца дня. «Видите, у нас есть глобальные переменные для предыдущего рабочего дня, для сегодняшнего дня, и для следующего рабочего дня. Они имеют формат YYMMDD, чтобы с ними было проще работать».
«Ага, ага, понял. Понял. А в каком они формате?»
"… Э-э-э… могу только предположить, что это год, месяц и день".
«Ага, ага, ладно. Отлично. Тогда я приступаю к работе».
После этого разговора у Като появилось нехорошее предчувствие. но он попытался от него избавиться. Консультант сказал, что всё настроено и готово. Он ведь точно знает, что делает, верно? Като выбросил это из головы и перестал беспокоиться, пока не настало время code review и он не обнаружил такой перл:
TH.DATE = R.DATES(EB.DAT.NEXT.WORKING.DAY)[1,6]:"01"
CALL CDT('ES00',TH.DATE,"-1C")
WTODAY = OCONV(DATE(),"DY") : FMT(OCONV(DATE(),"DM"),'R%2') : FMT(OCONV(DATE(),"DD"),'R%2')
IF TH.DATE EQ WTODAY THEN
Объясним вкратце, что здесь происходит:
- Берём следующий рабочий день и изменяем день на 01, чтобы получить первый день месяца.
- Меняем эту дату, вычитая 1 календарный день по календарю Испании.
- Берём дату с сервера и переводим её в формат YYYYMMDD, трижды вызывая команду Date.
- Если дата, вычисленная на этапе 2, равна дате, вычисленной на этапе 3, запускаем процесс.
Ну, вообще это… работает. По большей части. Если только по какой-то причине завершение дня не произойдёт после полуночи предпоследнего дня месяца — а это бывает не так редко. В таком случае код будет ошибочно думать, что это последний день месяца, и запустит создание отчёта. Что хорошо сочетается с ещё одной проблемой: если то же самое произойдёт в последний день месяца, то создание отчёта ошибочно не будет запущено. И самый лучший баг: если последний день месяца оказался воскресеньем, то календарь сервера никогда не будет устанавливаться на него, потому что он пропускает нерабочие дни.
Кстати о нерабочих днях: так как Inibank находится в США, нет никакой причины использовать календарь Испании. Да, месяца и недели будут такими же, но в испанском календаре ПО нужно указать банковские выходные в США, иначе программа будет продолжать работу. Наконец, как будто всего этого ещё не было достаточно, тройной вызов Date означает, что могут возникнуть разногласия при запуске ровно в полночь: значение месяца запрашивается перед полуночью, а день после неё.
Като добавил комментарий, предложив способ изменения кода:
IF R.DATES(EB.DAT.TODAY)[5,2] # R.DATES(EB.DAT.LAST.WORKING.DAY)[5,2] THEN
Пять минут спустя консультант подошёл к его столу. «Что означает эта правка?»
У Като в этот момент не было настроения спорить. «Твой код поломан, друг. Всё это не нужно».
«Понятно, понятно. Вообще-то это просто стандартная рабочая процедура для нашей отрасли. Ну да ладно».
Като сильно в этом сомневался, но просто пожал плечами. «Тогда отрасль ошибается. Я всё объяснил в комментарии».
«Ага. Да, я прочитал его. Но я прочитаю ещё раз». И он пропал столь же внезапно, как и появился.
Правки были внесены, Като одобрил код и консультант растворился в тумане.
Иногда, лёжа в кровати по ночам, Като задавался вопросом: действительно ли консультант понял, что сделал не так, или он просто согласился для вида, получил свой чек и продолжил где-то писать ужасный код за цену, вдвое превышающую зарплату Като? В тех банках, где нет сотрудников, способных проверить код.
Но не стоит вкладывать все деньги в биткоин. Там всё ещё хуже.
История вторая. Как это сделано
[Оригинал]
Люди любят есть хот-доги, пока не узнают, как они готовятся. Большинство этого не спрашивает, потому что не хочет знать и продолжает есть хот-доги. При разработке ПО нам иногда приходится спрашивать. Не только для того, чтобы решать проблемы, но и потому, что некоторые программисты боятся, что ПО в их автомобилях, несущихся по трассе со скоростью 100 км/ч, собрано из изоленты и палок. Вся наша отрасль плохо справляется со своими задачами.
Бретт работал системным аналитиком в медицинском исследовательском центре MedStitute. Для хранения и анализа данных MedStitute использовал проприетарное ПО под названием MedTech. Врачам и исследователям нравились результаты MedTech, но коллега Бретта Тайри знал, как они создавались.
У ПО не было доступа к бэкенду, и весь процесс разработки происходил в «программируемом мышью» GUI. Этот интерфейс выглядел так, как будто был написан человеком, изучавшим программирование копипастингом веб-сайтов эпохи 90-х, посмотревшим десять минут «Парка юрского периода» и искавшего ответы на StackOverflow, пока что-нибудь не удавалось скомпилировать. «Язык программирования» тоже демонстрировал аналогичный уровень продуманности философии дизайна. У каждого
if
обязательно должен был быть else
. В некоторых модулях использовались булевы значения, другие для обозначения значений false возвращали пустые строки. Из документации было непонятно, в какой ситуации случалось одно или другое. По сути, каждый оператор if
превращался в три оператора.Бретту нужно было начать новое исследование. Оно основывалось на простом множестве статистических данных и группировала пациентов с помощью рандомизированной переменной. Бретт поискал в списке переменную, которую можно было бы рандомизировать, но не нашёл нужной. Он предположил, что совершил ошибку и вернулся на несколько экранов назад, чтобы проверить её имя, скопировав его для поиска. Бретт вернулся к списку рандомизируемых переменных. Её там не было. Он присмотрелся к списку повнимательнее и заметил, что список рандомизируемых переменных содержал данные из полей с множественным выбором. Поле, которое ему нужно было рандомизировать, было основано на вычисляемом поле.
Бретт знал, что Тайри работал над другим проектом, который рандомизировался по вычисляемому полю, поэтому связался с ним в Slack. «Как ты закодировал эту случайную переменную? Medtech ведь этого не позволяет сделать?»
«Я говорю по конференц-связи, перезвоню позже», — написал Тайри.
Несколько минут спустя Тайри позвонил Бретту.
«Тебе нужно начать с двух полей. Допустим. назовём их
$variable_choice
, то есть вопрос с множественным выбором, и $variable_calced
, то есть твоё вычисляемое поле. Когда ты хочешь создать переменную, которая выполняет случайный выбор на основании вычисляемого поля, ты сообщаешь Medtech, что эта случайная переменная основана на $variable_choice
. Затем ты удаляешь $variable_choice
, и переименовываешь $variable_calced
в $variable_choice
»«Стоп, система позволяет сделать это, но не разрешает рандомизировать вычисленные поля каким-то другим способом? И она этого не проверяет?»
«Надеюсь, ничего не изменится, и она не начнёт проверять этого до завершения моего проекта», — ответил Тайри.
«Это исследование должно проходить в течение десяти лет. И его успешное завершение зависит от того, не посчитают ли разработчики эту уловку багом?»
«Мне удалось найти только такое решение. Дай знать, если найдёшь что-то получше».
Бретта не удовлетворил такой хак, и он вернулся к изучению документации. Он обнаружил решение «получше»: можно создать поле множественного выбора только для чтения с единственным вариантом значения по умолчанию — значением вычисляемого поля. К сожалению пользователь мог ненамеренно изменить список, ответив на вопрос с множественным выбором до вычисления значения вычисляемого поля.
В конце концов, единственное, что оставалось Бретту — сделать перерыв, пойти в кафетерий и купить пару хот-догов.
История третья. Портативность и крепёж
[Оригинал]
![](https://habrastorage.org/getpro/habr/post_images/52e/2cb/0c2/52e2cb0c2e0b8465a1b713aa60dfd474.jpg)
Много лун назад, когда PC имели тяжёлые корпуса из металла и пластика, Мэтту и его коллеге поручили оценить пакет ПО для грядущей операции отдела продаж. К сожалению, они с коллегой работали в разных офисах в пределах одного города. В ту эпоху ещё не существовало эффективных онлайн-инструментов для совместной работы, поэтому Мэтту регулярно приходилось ездить в другой офис, беря с собой PC. Это означало, что каждый раз нужно было отключать от корпуса 473 кабеля периферии, нести компьютер по коридорам и вниз по лестницам, ловить автобус, чтобы добраться до другого офиса, в котором он проделывал всё это в обратном порядке. Иногда неправильная организация труда заставляла эту пару работать по выходным, а значит, носить рабочие машины домой.
В процессе работы жёсткий диск на 20 МБ в компьютере Мэтта переполнился. Из своего офиса он отправил заявку в отдел ИТ. Для выполнения заявки назначили техника Гари, который спустя какое-то время появился в кубикле Мэтта, держа в руках новый жёсткий диск и отвёртку. Гари отправил Мэтта за кофе, чтобы сосредоточиться на своём «пациенте». После небольшого хирургического вмешательства PC Мэтта включился и заработал с большим жёстким диском.
За день до дедлайна проекта Мэтт почти завершил свою долю работы. Ему оставалось внести всего несколько дополнений в свой отчёт, а потом скопировать его на гибкие диски и отправить в отдел продаж. Вернув свой PC в кубикл и подсоединив провода, он включил питание и услышал хлопок. PC был мёртв и не подавал признаков жизни.
После панического звонка в отдел ИТ, в его кабинете снова появился Гари с отвёрткой. Вскрыв корпус PC, он сразу же завопил: «Постойте-ка! Вы что, куда-то таскали компьютер?»
Мэтт нахмурился. «Ну да. А что, в этом дело?»
«Да уж конечно! Вы не должны были этого делать!», — начал ругаться Гари. «Жёсткий диск начал болтаться и закоротил всё внутри!»
Мэтт наклонился над Гари, чтобы самому увидеть внутренности компьютера. Он сразу заметил, что новый жёсткий диск был «закреплён» на скотч.
«Стоп! Это вы не должны были этого делать!» Мэтт показал на кусок скотча. «Мне что, на всякий случай стоит обратиться к вашему руководителю?»
Лицо Гари сморщилось. «Мне не дают нужные крепления!»
«Тогда найдите того, у кого они есть!»
Учитывая надвигающийся дедлайн, с разрешения начальника Мэтт передал свою заявку ещё выше. Почти сразу же клейкую ленту заменили на настоящий крепёж. Он так никогда и не понял, почему у сотрудников отдела ИТ не было доступа к необходимому оборудованию; он предположил, что это была блестящая идея какого-то идиота для экономии средств. Мэтт мог только догадываться, какие ещё отчаянные импровизации позволяли обеспечивать работоспособность их ИТ-инфраструктуры, и как долго бы они оставались незамеченными, если бы не сломался его PC.
История четвёртая. Вот как на твой мозг влияет PL/SQL
[Оригинал]
Вечным чемпионом среди самых странных и неудачных решений навсегда будет оставаться Oracle. Сегодня мы рассмотрим небольшой код на PL/SQL.
PL/SQL — это странный язык, смесь SQL и Procedural (процедурного) Language (языка) с приклееной сбоку объектно-ориентированностью. Синтаксису превосходно удаётся создать впечатление, что он был разработан в 1970-х, и каждая новая функция или изменение языка продолжают эту традицию.
Структура каждого модуля кода на PL/SQL строится на основе блоков. Каждый блок представляет собой самостоятельное пространство имён. Вкратце его анатомия выглядит так:
DECLARE
-- variable declarations go here
BEGIN
-- code goes here
EXCEPTIONS
-- exception handling code goes here, using WHEN clauses
END;
Если вы пишете хранимую процедуру или обработчик событий, то заменяете ключевое слово
DECLARE
на CREATE [OR REPLACE]
. Также можно вкладывать блоки внутрь других блоков, поэтому довольно часто можно увидеть код, структурированный таким образом:BEGIN
DECLARE
--stuff
BEGIN
--actions
END;
--more actions
END;
Да, довольно быстро это начинает запутывать. И да, если вы хотите обеспечить хотя бы приблизительно структурированную обработку ошибок, то обязаны начинать вкладывать блоки друг в друга.
Язык и база данных имеют и другие забавные особенности. До версии 12c у них не было типа столбца
IDENTITY
. В предыдущих версиях необходимо было использовать объект SEQUENCE
и писать процедуры или обработчики событий, выполняющих принудительное автоматическое нумерование. Обычно для присваивания значения переменной использовался оператор SELECT INTO…
. Бонус: Oracle SQL всегда требует указания таблицы в операторе FROM
, поэтому необходимо использовать придуманную таблицу dual
, например так:CREATE TRIGGER "SOME_TABLE_AUTONUMBER"
BEFORE INSERT ON "SOME_TABLE"
FOR EACH ROW
BEGIN
SELECT myseq.nextval INTO :new.id FROM dual;
END;
:new
в данном контексте обозначает строку, для которой мы выполняем автоматическую нумерацию. В старых версиях Oracle это был «обычный» способ создания столбцов с автоматической нумерацией. Бенуа обнаружил другой, немного менее обычный способ выполнения той же операции:CREATE OR REPLACE TRIGGER "SCHEMA1"."TABLE1_TRIGGER"
BEFORE INSERT ON "SCHEMA1"."TABLE1"
FOR EACH ROW
BEGIN
DECLARE
pl_error_id table1.error_id%TYPE;
CURSOR get_seq IS
SELECT table1_seq.nextval
FROM dual;
BEGIN
OPEN get_seq;
FETCH get_seq
INTO pl_error_id;
IF get_seq%NOTFOUND
THEN
raise_application_error(-20001, 'Sequence TABLE1_SEQ does not exist');
CLOSE get_seq;
END IF;
CLOSE get_seq;
:new.error_id := pl_error_id;
END;
END table1_trigger;
Тут происходит очень многое. Для начала заметьте, что в разделе
DECLARE
содержится оператор CURSOR
. Курсоры позволяют итеративно переходить между записями. Они очень затратны и в мире Oracle это ресурс, который нужно освобождать.Этот обработчик событий (триггер) использует вложенный блок безо всяких на то причин. Также он использует дополнительную переменную
pl_error_id
, без которой можно обойтись.Но по-настоящему странная часть заключается в блоке
IF get_seq%NOTFOUND
. Всё довольно просто: он проверяет условие, что курсор не вернул строку. Такого для этого курсора не может случиться даже теоретически, поэтому операции внутри никогда не выполняются. Последовательность всегда возвращает значение. И это хорошо, учитывая тот код, который идёт дальше.raise_application_error
— это аналог «throw» в Oracle. Этот оператор поднимается по стеку из выполняемых блоков, пока не находит раздел EXCEPTIONS
для обработки ошибки. Заметьте, что мы закрываем курсор после этого оператора — то есть, на самом деле мы никогда не закрываем курсор. Курсоры, как сказано выше, затратны, и Oracle позволяет использовать только ограниченное их количество.Здесь мы видим странный пример того, как разработчик пытается защититься от ошибки, которая не может произойти, таким образом, которая со временем приведёт к новым ошибкам.
История пятая. Логины с двойным шифрованием
[Оригинал]
Создание аутентификации для веб-API — это сложная задача, но она имеет множество устоявшихся решений. Самое трудное на самом деле — выбрать один из различных вариантов, после чего достаточно просто добавить компонент.
При правильной реализации система не зависит от типа клиента. Я могу получать доступ к сервису через браузер, в толстом клиенте или через cURL. При неправильной реализации вы получаете то, что случилось с Амирой.
Она решала задачу вытягивания из бэкенда нужной ей статистики, но не могла разобраться в способе аутентификации. Поэтому она изучила код фронтенда, чтобы понять, как он выполняет аутентификацию:
crypt = new JSEncrypt();
crypt.setPublicKey('<removed>');
challenge = "<removed>";
function doChallengeResponse()
{
document.loginForm.password.value.replace(/&/g, '%26');
document.loginForm.password.value.replace(/\\+/g, '%2B');
document.loginForm.password.value = crypt.encrypt(document.loginForm.password.value);
document.loginForm.response.value = document.loginForm.password.value;
document.loginForm.password.value = '';
document.loginForm.submit();
}
С одной стороны, я могу предположить, что этот код очень стар, учитывая
document.loginForm
, используемый для взаимодействиями с DOM-элементами. С другой стороны, JSEncrypt
был впервые выпущен в 2013 году, что даёт нам максимальную планку возраста.Мы передаём параметры доступа бэкенду с помощью отправки формы, которая, по мнению разработчика кода требовала очистки — все
&
и +
в пароле заменяются, но… это необязательно, потому что форма должна выполнять запрос POST
и, кроме того, мы зашифровали данные.Вот что я думаю. Код и в самом деле довольно стар. Разработчик скопировал его из поста на StackOverflow примерно за 2005 год, в котором не использовалось шифрование и отправка формы через
POST
. Год за годом в него добавлялись небольшие изменения. но базовый механизм никогда не менялся.Амира проверила историю, и обнаружила, что в предыдущей версии шифрование не использовалось. Код конкатенировал пароль с переменной
challenge
, оба они хешировались MD5, после чего он передавал это в сеть, и всё как бы работало, если слишком сильно над этим не задумываться.Была и хорошая сторона дела: когда Амира разобралась, как получать нужные ей куки, срок жизни этого токена на сервере никогда не истекал, поэтому она могла хранить его, отправлять запросы через cURL и больше не связываться с веб-формой. Ей не нужно было беспокоиться о компрометировании куки при передаче, потому что приложение использует и всегда использовало SSL/TLS.