Это случилось в ту пору, когда я, имея весьма, как мне казалось, посредственный опыт в разработке, искал место, где можно из недоджуниора эволюционировать (или мутировать) хотя бы в уверенного джуниора. Неисповедимыми путями Господними такое место нашлось, в довесок к месту прилагался проект, и “олдскульный” программер, который за свою карьеру систем написал больше чем девок перепортил. “Отлично! Проект, а следовательно деньги на ЗП есть, наставник прилагается, живем!” — подумал я, но затем, как в описании к типичному хоррору, герои в темной тьме столкнулись с ужасным ужасом…
Обо всем по порядку:
1. Размер имеет значение
Разработку начали на самописном кем-то когда-то php-движке, для хранения данных использовали (тут вы можете подумать MySQL\PostgreSQL\SQLite\MongoDB\Что-то-там-еще-но-обязательно-с-суффуиксом DB-иначе-пацаны-не-поймут, а вот и не угадали) api-шлюз.
“Нафига, используя php, вы приделываете к нему еще какой-то api-шлюз, и храните на нем данные? Не проще ли работать с api напрямую из js-кода? Или использовать СУБД+PHP?” — спросит видавший виды читатель. И будет прав. Но в ту пору я, не видавший еще видов, так не думал, ну кто ж его знает, крутые пацаны так наверное делают, и “олдскульным” программерам виднее.
Как мне было далее разъяснено:
- Шлюз = безопасность, никто не войдет и не выйдет просто так
- Шлюз = защищенное хранение данных, просто так не залезешь, +бекапы
- Шлюз = скорость, работает быстро и без сбоев, проверено временем
- Авторитетная точка зрения “олдскульных” программистов гласит — ваш php дырявый весь, любое веб-приложение взломано по умолчанию, поэтому нечего данные хранить рядом с ним
Характерной особенностью api-шлюза являлось то, что json-данные передавались в get-запросе. Да-да, те самые милые сердцу json-объекты, подвергались url-кодированию, и клались в query string. И все бы хорошо, как вдруг однажды… длины get-запроса перестало хватать. Тупо не влезал туда url-кодированный json, каналья! “Олдскульный” программер, почесав затылок, спросил:
“Шо будем делать? Подрос наш json, а мы и не заметили…”.
“Ну, эээ, может тогда в post их будем передавать?” — предложил я, так вроде правильней будет.
“Ок, передавайте в post”.
Это был атас номер раз.
2. Тайм-энд-бекап-менеджмент
Чтобы прикрутить новую функциональность в проект, надо было реализовать
соответствующие CRUD-запросы на шлюзе, чем собственно и занимался наш “олдскульный” товарищ. Проблема заключалась в том, что занимался он этим раз в 3 дня, выдавая — “Готово, проверяй”. Проверки временами показывали что работало не все, например получение списка — ок, добавление нового элемента — не ок. На исправление и доработку уходило еще какое-то время, после чего можно было выпускать функционал в массовый доступ. Предложение самому заняться реализацией запросов на шлюзе, потому что это как минимум быстрее, было отклонено, потому что “там сложно, ты не разберешься”. Итогом такого подхода стало замыкание работы “на себя”. Если, например, нужно было что-то массово исправить в БД, то, выбирая между 3-х дневным ожиданием и реализацией исправлений самому через запросы — я выбирал 2-й вариант. Заказчики ждать особо не любили, новые вводные прилетали стабильно. Одну из таких вводных, а именно массовое проставление пользователям некоего признака мне и поручили реализовать, времени на все про все — час, начальство ждало красивый отчет. Здесь нас поджидает атас номер два-с.
Дело в том, что формат json-данных, передаваемые в запросах, предполагал лишь несколько обязательных полей, все остальные были произвольными, четкой и окончательной структуры не существовало. Например, для добавления пользователя я передавал json вида:
POST /api/users
{
"email":"ivanov@mail.ru",
"password":"myEmailIsVeryBig",
"name_last":"Иванов",
"name_first":"Иван",
"name_middle":"Иваныч",
"birth":"01.01.1961",
//а вот тут следует вольноопределяемая часть, что считаем нужным - то и отдаем
"living_at":"ул.Сусаниа, д.3 к.4 кв.24",
"phone_num":"+70000000000"
}
Та необязательная часть, которую передавали в запросах добавления\обновления — сохранялась и отдавалась в полном составе (о том как это было реализовано — расскажу ниже). Суть да дело, время на месте не стоит, надо бы и задачу решить — обновить пользователей, проставить им метки. Но не гонять же каждый раз всю структуру? Надо проверить! Протестировал на себе — передал в запросе на обновление только одно поле, проверил, поле появилось, остальные данные на месте. Дело за малым — зациклить и обновить остальных.
Скрипт тихонько пыхтел, принимая и отдавая данные, и вроде бы все шло хорошо… как вдруг — звонок. “Мы не видим ФИО пользователей в системе!” — сообщают с того конца провода. “Да ну нафиг! Нормально ж отрабатывало!” — по спине пробежал неприятный холодок. Дальнейшее расследование показало, что действительно, в строке ФИО значилось “”, хотя все остальные данные были на месте. Что делать в такой ситуации? Разворачивать бекап!
“Товарищ “олдскульный” программер, уи хэв э проблем хир! Нужно бекап! Когда последний актуальный сделан?” — спрашиваю.
“Э-э-э… Сейчас посмотрю…. Не, бакапа нету”.
Спасло ситуацию то, что парой часов ранее я доработал и протестировал модуль с отчетами, у меня была csv-шка со всеми необходимыми данными, в течение еще одного часа порядок был восстановлен.
Отсутствие внятной документации, описания алгоритмов работы, входных проверок на валидность, и что самое главное — бекапов БД — атас номер два-с.
С тех пор бекапы стали сниматься каждый день.
3. Deep striking
Шатко-валко, но работа двигалась, проблемы решались, какие-то быстрее, какие-то медленнее, как вдруг… заказчики спохватились, что система лежит на не пойми чьих серверах, и за такое отношение к ПДн и организации мероприятий по ЗИ в ИСПДн их по головке не погладят. Надо переносить сервера к себе.
Почему изначально система не была передана? У руководства была одна страсть — централизация. Руководство мечтало о системе которая будет делать всё! Нужно тебе детёнка в школу пристроить? Заходишь в систему, в специальный кабинет, там подаешь заявление. Нужно тебе, скажем пиццу заказать — заходишь в систему, в другой специальный кабинет, подаешь заявление на пиццу. Может тебе общения с прекрасными дамами\кавалерами захотелось? К вашим услугам третий специальный кабинет — там тоже заявление подаешь.И так до бесконечности.
Преимущества — один логин и пароль на всё, данные надежно и безопасно хранятся на шлюзе. Даже бекапы есть. И, заметьте, никто у нас эту систему не отнимет! А даже если отнимет — что дальше? Все равно не разберутся в системе защиты от “олдскульных” программистов — там сложно всё.
VDS с системой выгрузили, отнесли к заказчикам, они ее развернули, все танцует и поет, красота!
И тут меня накрыло волной любопытства и некоторых подозрений.
Если наше веб-приложение дырявое, то где же данные? Неужели остались на других серверах? А если систему решат закрыть извне — то все рухнет?
Простая проверка показала, что данные, как и сами обработчики шлюза стояли на этом же сервере. И, нет, их не перенесли туда по причине передачи сервера, они там были всегда.
Теперь у меня в распоряжении была та самая секретная “олдскульная” разработка, которую я и принялся исследовать. Конечно, крутого реверс-инжинирнга в стиле статей журнала “Хакер”, с ollydbg, смещениями, и прочими крутыми штуками не получилось, поэтому делюсь тем, что есть.
Собственно разработка была вполнена на python, остались только .pyc-файлы, которые легко декомпилировались обратно в читаемый код. Скажу честно, много времени, целых 25 минут, ушло на то, чтобы разобраться как это работает.
Итак, сложная система, разработанная “олдскульным” программером, в которой мало кто может разобраться, состоит из:
- Скрипта, обрабатываемого апачем, который собственно и получал запрос. Что делал данный скрипт? Открывал соединение на определенный порт localhost`а и передавал туда запрос со всеми его данными. Всё. Интересности идут дальше.
- Серверной части, обрабатывавшей запросы от скрипта. Логика его действий была достаточно интересна. Во-первых, в коде не было никаких манипуляций с данными, и запросов в БД, вместо этого вызывались функции БД на PL\SQL. Вся логика, проверки, и прочее, все было заложено в функции БД. 50% скрипта составлял словарь содержащий имя запроса, сопоставленную ему функцию, и имена параметров функции, которые должны были соответствовать данным, переданным в строке get-запроса. JSON-данные, если они были нужны, передавали как отдельный параметр. Особенностью организации серверной части явилось резервирование подключения при аутентификации пользователя. Если логин и пароль обнаружены в БД — генерировался ИД сессии, а экземпляр открытого подключения складывался в словарь (и убивался по таймауту в 10 минут, чтобы не убивался — был специальный метод на продление жизни сессии), ключом являлся ИД сессии, который в БД напрямую не хранился. Как же именно связан ИД сессии с данными пользователя? Ведь есть запрос на получение данных, в который ИД пользователя не передается? Он работает, а значит что-то тут не так.
Очень сложная разработка давалась сознанию с трудом и не спешила раскрывать давно утерянные секреты мастеров прошлого.
Невероятным (Go to > Definition, спасибо PhpStorm за понимание PL\SQL), непостижимым разуму обывателя страданием Истинное Знание Утерянной Цивилизации Олдскульных Программистов было все же обретено. В общем — при подключении, в функции проверки данных аутентификации генерировалась временная таблица, в которой хранился ид пользователя.
Это было только началом, примерный список найденных серьезных уязвимостей:
- DDoS с помощью массовой аутентификации (подключения резезвировались, и, следовательно, упирались в лимит соединений СУБД, что с учетом имеющейся возможности продления времени жизни сессии позволяло полностью забить память подключениями, и работа новых пользователей в системе станет невозможна);
- отсутствие защиты от брутфорса (кол-во неудачных попыток входа не детектится, не хранится, не проверяется;
- отсутствие контроля действий с сущностями (например, список документов, запрошенный пользователем, выдавался с учетом организации, к которой пользователь привязан, при этом, если знать ИД документа, то можно успешно выполнить запрос на обновление\удаление документа, а список пользователей, хорошо хоть без паролей, которые, кстати, хранились в БД в открытом виде, без хеширования, мог получить вообще кто угодно).
И еще одна серьезная проблема — не формализованная схема хранения данных. Как и обещал ранее — рассказываю о хранении “любых полей” из JSON. Нет, они не хранились как строка в таблице. Они разбивались на key-value пары и хранились в отдельной таблице. Например, для пользователей было 2 таблицы — users, и users_data (string key, string value) — где собственно и хранились данные. Итогом такого подхода стало увеличение времени при сложных выборках из БД.
Собственно этого хватило, чтобы принять и привести в жизнь решение о переводе системы на новое api, понятное, документированное, и поддерживаемое.
Мораль
Возможно, эта система являет собой “легаси”, а “олдскульный” программер, создавший ее — суть хранитель легаси.
Но тем не менее выводы следующие:
- Если вам говорят “там сложно, ты не разберешься” — значит там полный атас
- Если давят авторитетом — значит что-то нечисто
- Доверяй, но проверяй — безопасность — не состояние, безопасность — процесс, притом непрерывный, посему лучше убедиться в соответствии декларируемых качеств действительности, чем потом узнать что все пользователи вдруг стали “Ивановыми Иванами Иванычами”, а бакапов нету.
Комментарии (18)
StrangerInTheKy
03.05.2019 17:57+1для хранения данных использовали (тут вы можете подумать MySQL\PostgreSQL\SQLite\MongoDB\Что-то-там-еще-но-обязательно-с-суффуиксом DB-иначе-пацаны-не-поймут, а вот и не угадали)
… ммм… дайте подумать…
функции БД на PL\SQL
А вот и угадали ;)
В общем — при подключении, в функции проверки данных аутентификации генерировалась временная таблица, в которой хранился ид пользователя.
Эмэсэскуэльщик, быстро перековавшийся в ораклиста? У вас должно было быть очень весело…
И еще одна серьезная проблема — не формализованная схема хранения данных.
Что такое «неформализованная схема» применительно к реляционной СУБД? Там внутри все очень даже формализовано.
Как и обещал ранее — рассказываю о хранении “любых полей” из JSON. Нет, они не хранились как строка в таблице. Они разбивались на key-value пары и хранились в отдельной таблице. Например, для пользователей было 2 таблицы — users, и users_data (string key, string value) — где собственно и хранились данные. Итогом такого подхода стало увеличение времени при сложных выборках из БД.
По вашему описанию непонятно, что такое «хранение “любых полей” из JSON» и как оно потом используется, и детали реализации тоже не очень ясны, но судя по имеющейся информации — по факту это реализовано лучше, чем предлагаете вы. А причины тормозов выяснить не сложно — оракл в этом плане обладает очень хорошим инструментарием. Скорее всего, просто не было никого, кто умеет им пользоваться.MacIn
03.05.2019 18:29Вероятно, таблица users_data имеет поля key, value и fk user_id.
Т.е. мы сохраняем структуру вида a:«a data», b:«b_data», c:«c_data» для пользователя с id 123 и этому будут соответствовать 3 строки в users_data (опуская pk):
123, a, a_data
123, b, b_data
123, c, c_data
…
Тогда чтобы получить все данные для пользователя, надо выбирать по where user_id=123
У меня на работе в legacy коде одного продукта используется такой подход: есть дополнительно поле в каждой строке таблицы, в котором данные хранятся в двоичном serialized виде с признаками начала ключа и начала значения. Разумеется, это «листовые» данные, по ним не производится поиск.Dmitry89 Автор
03.05.2019 18:58Да, вы правильно всё поняли, если данные представляют собой «довесок», используемый только при получении конкретной сущности — ок. А схема хранения — пихай все что хочешь, мне лень переделывать каждый раз — не ок.
Dmitry89 Автор
03.05.2019 18:48Скажем так, это был не Oracle и не MsSQL, это был PgSQL.
Не формализованная схема — может я не совсем понятно выразился, и неверно подобрал определение. Смысл в следующем — есть некая сущность, пусть будет, скажем, входящее письмо, у которого есть следующие поля — «От кого»,«Кому»,«Куда», «Содержание», «Дата отправления», «Дата получения», «Доставил», «Получил». Для реализации схемы создавалось 2 таблицы — letters, и letters_data. В letters поля — id, from (fk users), to (fk users). Остальное в таблице letters_data, в которой есть 4 поля — id, letter_id, key_value, следовательно для хранения оставшихся полей будет 6 записей в letters_data, вместо того чтобы сделать alter table и добавить столбец.
Хранение любых полей json — передаем в структуре входящего письма кроме полей, означенных выше еще 1 поле, например «answer_on» (ответ на другое письмо, в поле по логике должен лежать id письма), это поле и залетает в letters_data седьмым.
Выяснить причины тормозов действительно несложно, но когда вам нужно будет, например получить все письма, отправленные за период, и принятые кем-то — начинается магия с кастами. Или, например, ссылочная целостность в данном случае тоже страдает.
Пример с письмами, конечно, очень условный.
Да и основная претензия к данной разработке такова, что архитектура представляет собой сборную солянку, чтобы поддерживать которую надо владеть php\python\js\plsql, это обстоятельство в условиях отсутствия документации на «черный ящик» — сулит определенные эротические похождения.NoRegrets
03.05.2019 21:14+2Этот паттерн называется EAV. Обычно применяется когда во время разработки неизвестен список хранимых полей, он определяется в процессе работы конечным клиентом.
VladimirAndreev
04.05.2019 11:41К счастью, много кто умеет хранить json в полях:-)
Dmitry89 Автор
04.05.2019 13:02Эта система, судя по подходу, разрабатывалась в то время, когда СУБД JSON не поддерживали от слова «совсем».
oldcastor
03.05.2019 20:17Возможно в этом месяце буду перекинут
через бедрона поддержку легаси возрастом лет 15 в помощь товарищу. Там весело: 3 базы (mssql и 2 оракла) с дублированием данных, неактуальными процедурами с тоннами хардкода без каких-либо намеков на комментарии и дблинками в самых неожиданных местах, взаимосвязи с несколькими параллельно работающими проектами (дублирование данных из них тоже есть) и, на сладкое, полное отсутсвие не то что техзадания, даже людей с точным пониманием процессов там происходящих. В общем софт работает (сами понимаете как) и никто толком не знает как он должен работать. Тут то и появится повод набросать статейку))immaculate
04.05.2019 11:08Думаю, таких историй у каждого в загашнике, кто проработал от 10 лет. У меня ощущение, что как минимум 95% существующего кода просто ужасно.
С одной стороны, существует мнение, что «плохой код — это тот, который написан не мной» (другими словами, не надо жаловаться, сами пишете не лучше, все субъективно). С другой стороны, я отношу себя к людям, считающим, что объективно плохой код существует и преобладает (сам тоже часто пишу плохой код, несмотря на все старания).
Ну вот как быть, например, с такими фактами, что большинство JS приложений, даже очень простых, непрерывно потребляют от 2 до 60 (при нормальной работе, в исключительных случаях доходит до 100 и более) процентов CPU. Какой-нибудь таймер на JS (для подсчета времени, затраченного на задачу) может запросто непрерывно съедать десятки процентов процессорного времени. Да, это не смертельно. Только вот ноутбук от батарейки работает значительно меньше, где-то раза в два, чем если не использовать ни кусочка JS кода.
Или вот типичные ошибки, которые многие повторяют из раза в раз. Например, при каждом изменении требований, тупо добавлять в код еще одно вложенное условие или цикл, вместо того, чтобы окинуть всю архитектуру в общем, и подумать, как лучше переработать код, чтобы он отвечал изменившемуся требованию. В итоге, получаются огромные классы или или методы или функции, в которых можно увидеть 4-5 вложенных условий, тогда как уже три уровня тяжело читать и понимать.
Вот хорошая эмоциональная статья о таком: https://www.stilldrinking.org/programming-sucks
VolCh
04.05.2019 11:58На самом деле хороший подход реализовывать требования "в лоб". Но после реализации должна быть фаза рефакторинга. Ну или сначала провести рефакторинг под планирующуюся реализацию, а потом реализовывать, но это чревато:
- закопаться в рефакторинге, так и не выйдя на реализацию к дедлайну
- при реализации выявить, что план под который делался рефакторинг был неудачным, не учитывали какие-то нюансы.
Сделав сначала реализацию, можно спокойно заниматься рефакторингом, в крайнем случае не завершая его к дедлайну, ведь фича уже реализована. Просто заводим задачу на рефакторинг, а основную задачу сдаём.
Dmitry89 Автор
04.05.2019 13:05Сначала делаем чтобы было, затем чтобы было правильно, затем чтобы было быстро, как-то так, видимо.
Dmitry89 Автор
04.05.2019 13:10«плохой код — это тот, который написан не мной»
Есть и такая крайность, хотя свои разработки никогда не возводил на пьедестал почета. Бомбит от подхода «у меня все настолько хорошо сделано что код выложил на порнохаб вместо гитхаба», особенно когда выясняется что разработка толком-то свою функцию не выполняет. Если разработка выполняет свое предназначение (пусть даже состоит из костылей) — лааадно, надо просто отрефакторить, это интересный процесс, а вот когда разработка представляет собой граблю — как-то грустненько становится.
arozhankov
«Олдскула» то уволили в итоге за вредительство?
Dmitry89 Автор
Нет, я сам ушёл оттуда, поскольку не видел возможностей для развития, а олдскул на месте.