Первые шаги в aiohttp

Введение

Привет, меня зовут Артём и я работаю бекендером в KTS. Компания уже 3 года проводит летние и зимние курсы по разработке, а в феврале этого года прошла очередная бесплатная backend-школа от KTS. В ее рамках студенты изучали инструменты и технологии, которые используют разработчики нашей компании, общались с менторами и делали итоговый проект — чат-бота в стиле “Моя игра”, который защищали в конце курса. После курса отличившихся студентов мы приглашали на стажировку.

Школа состояла из 6 лекций, шаг за шагом погружавших студентов в мир веб-разработки. На них были рассмотрены такие темы как сетевые протоколы, взаимодействие backend-а и frontend-а, компоненты веб-сервера и многое другое. Лейтмотивом курса было изучение асинхронного веб-программирования на Python,  в частности изучение фреймворка aiohttp.

Для поступления на курс нужно было пройти комплексный тест на знания в области веба и python-а, так что студенты пришли учиться с хорошим начальным уровнем знаний. Однако, во время курса выяснилось, что не все темы даются одинаково легко. Самыми трудными для понимания темами стали:

  1. Асинхронное программирование

  2. Работа с СУБД

  3. Деплой приложения

Студенты задавали достаточно разные по уровню понимания вопросы, начиная от “Как создать отложенную задачу, используя только asyncio?” и заканчивая “Почему нельзя использовать Django для асинхронного программирования?” (имелась в виду полностью синхронная версия Django). В коде наши менторы тоже находили ошибки, связанные с недостаточным пониманием предмета, например, использование синхронного драйвера для базы данных в асинхронном проекте.

По результатам курса, я решил написать небольшой туториал, рассказывающий о создании базового aiohttp-сервиса с нуля и затрагивающий самые сложные для студентов вопросы: как сделать асинхронное python-приложение, как работать с базой данных и как разложить свой проект в интернете. 

В цикле статей мы рассмотрим следующие темы:

  1. Архитектура веб-приложения

  2. Асинхронная работа с базой данных и автоматические миграции

  3. Работа с HTML-шаблонами с помощью Jinja2

  4. Размещение нашего приложения в Интернете с помощью сервиса Heroku

  5. А также сигналы, обработку ошибок, работу с Docker’ом и многое другое.

Эта статья – первая из трех, и ее цель – помочь начинающим aiohttp-программистам написать первое “hello-world” приложение.

В этой статье мы напишем небольшое веб-приложение на aiohttp — стену с отзывами, где пользователь может оставить мнение о продукте.

Мы пройдем по шагам:

Создание проекта

Все команды в статье были выполнены в операционной системе OSX, но также должны работать в любой *NIX системе, например в Linux Ubuntu. Во время разработки я буду использовать Python 3.7.

Давайте создадим папку aiohttp_server, которая в дальнейшем будет называться корнем проекта. В ней создадим текстовый файл requirements.txt, который будет содержать все необходимые для работы приложения зависимости и их версии.  Запишем в него следующие модули:

aiohttp==3.7.3 # наш фрейворк aiohttp-jinja2==1.4.2 # модуль для работы с HTML-шаблонами

Создадим виртуальное окружение – что-то вроде песочницы, которое содержит приложение со своими библиотеками, обновление и изменение которых не затронет другие приложение, и установим в него наши зависимости:

cd {путь_до_папки}/aiohttp_server python3 -m venv venv source venv/bin/activate

После этого в начале строки терминала должна появится надпись (venv) — это означает что виртуальное окружение успешно активировано. Установим необходимые модули:

pip install -r requirements.txt

Структура проекта

Создадим в папке aiohttp_server следующую структуру:

├── app │   ├── __init__.py │   ├── forum │   │   ├── __init__.py │   │   ├── routes.py  # тут будут пути, по которым надо отправлять запросы │   │   └── views.py  # тут будут функции, обрабатывающие запросы │   ├── settings.py ├── main.py  # тут будет точка входа в приложение ├── requirements.txt └── templates    └── index.html  # тут будет html-шаблон страницым сайта

Теперь откроем файл main.py и добавим в него следующее:

from aiohttp import web  # основной модуль aiohttp import jinja2  # шаблонизатор jinja2 import aiohttp_jinja2  # адаптация jinja2 к aiohttp  # в этой функции производится настройка url-путей для всего приложения def setup_routes(application):    from app.forum.routes import setup_routes as setup_forum_routes    setup_forum_routes(application)  # настраиваем url-пути приложения forum  def setup_external_libraries(application: web.Application) -> None:    # указываем шаблонизатору, что html-шаблоны надо искать в папке templates    aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader("templates"))  def setup_app(application):    # настройка всего приложения состоит из:    setup_external_libraries(application)  # настройки внешних библиотек, например шаблонизатора    setup_routes(application)  # настройки роутера приложения  app = web.Application()  # создаем наш веб-сервер  if __name__ == "__main__":  # эта строчка указывает, что данный файл можно запустить как скрипт    setup_app(app)  # настраиваем приложение    web.run_app(app)  # запускаем приложение

После предварительной настройки можно создать первый View.

Первый View

View — это некий вызываемый объект, который принимает на вход HTTP-запрос — Request и возвращает на пришедший запрос HTTP-ответ — Response.

Http-запрос содержит полезную информацию, например url запроса и его контекст, переданные пользователем данные и многое другое. В контексте запроса содержатся данные, которые мы или aiohttp добавили к этому запросу. Например, мы предварительно авторизовали пользователя — чтобы повторно не проверять авторизацию пользователя из базы во всех View и не дублировать код, мы можем добавить объект пользователя в контекст запроса. Тогда мы сможем получить нашего пользователя во View, например, так: request[‘user’].

HTTP-ответ включает в себя полезную нагрузку, например, данные в json, заголовки и статус ответа. В простейшем View, который из примера выше, всю работу по формированию HTTP-ответа выполняет декоратор @aiohttp_jinja2.template(«index.html») . Декоратор получает данные из View, которые возвращаются в виде словаря, находит шаблон index.html (о шаблонах написано ниже), подставляет туда данные из этого словаря, преобразует шаблон в html-текст и передает его в ответ на запрос. Браузер парсит html и показывает страницу с нашим контентом.

В файле views.py в папке app/forum напишем следующий код:

import aiohttp_jinja2 from aiohttp import web  # создаем функцию, которая будет отдавать html-файл @aiohttp_jinja2.template("index.html") async def index(request):    return {'title': 'Пишем первое приложение на aiohttp'}

Здесь создается функциональный View (function-based View). Определение “функциональный” означает, что код оформлен в виде функции, а не классом (в следующей части мы коснемся и class-based View).

Рассмотрим написанную функцию детальнее: функция обернута в декоратор @aiohttp_jinja2.template(«index.html») — этот декоратор передает возвращенное функцией значение в шаблонизатор Jinja2, а затем возвращает сгенерированную шаблонизатором html-страницу как http-ответ. В данном случае возвращенным значением будет словарь, значения которого подставляются в html-файл index.html.

Отдельно стоит заметить, что объект запроса request передается как аргумент функции index. Мы не используем request в этой функции, но будем использовать в дальнейшем.

HTTP-запрос отправляется на конкретный url-адрес. Для передачи HTTP-запроса в нужный View необходимо задать эту связь в приложении с помощью Route.

Первый Route

Route — это звено, связывающее адрес, по которому был отправлен запрос и код View, в котором этот запрос будет обработан. То есть, если пользователь перейдет в корень нашего сайта (по адресу /), то объект запроса будет передан в View index и оттуда же будет возвращен ответ. Подробней про Route можно прочитать тут.

В файл routes.py необходимо добавить следующий код:

from app.forum import views  # настраиваем пути, которые будут вести к нашей странице def setup_routes(app):    app.router.add_get("/", views.index)

Первый Template

Теперь нам осталось только добавить в templates/index.html код верстку нашей страницы. Его можно найти по этой ссылке.

Template — это html-шаблон, в который подставляются данные, полученные в результате обработки запроса. В примере в коде View отдается словарь с ключом title, шаблонизатор Jinja2 ищет в указанном html-шаблоне строки {{title}} и заменяет их на значение из словаря по данному ключу. Это простейший пример, шаблоны позволяют делать намного больше: выполнять операции ветвления, циклы и другие операции, например, суммирование. Примеры использования можно посмотреть в документации jinja2.

Запуск приложения

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

python3 main.py

Вы должны увидеть следующий текст в консоли. Он означает, что сервер запущен на порту 8080.

======== Running on http://0.0.0.0:8080 ========  (Press CTRL+C to quit)

Давайте теперь посмотрим результаты нашей работы! Для этого перейдите по адресу http://0.0.0.0:8080 в браузере. Вы должны увидеть первую версию нашего приложения. При клике на кнопку “Отправить” должно возникнуть сообщение о том, что отзыв отправлен.

Поздравляю! Вы успешно создали первое приложение на aiohttp!

Заключение

В статье рассмотрено создание простого приложения на aiohttp, которое принимает запрос пользователя и отдает html-страницу. Мы затронули:

  • Настройку виртуального окружения

  • Базовую настройку проекта на aiohttp

  • Создание View

  • Создание Route

  • Использование html-шаблонов

Наше приложение представляет собой простой веб-сервер, отдающий html-страницу по запросу — в нем нет никакого взаимодействия с базами данных, его структура максимально проста и оно недоступно пользователям в Интернете. В следующих статьях мы разберем, как вырастить из нашей заготовки “настоящее” веб-приложение на aiohttp и опубликовать его в Интернете.

Весь код статьи можно найти на гитхабе.

Пользуясь случаем, приглашаю всех читателей, интересующихся веб-разработкой, к нам на бесплатные занятия школу KTS. А для более опытных читателей сейчас идет запись на продвинутые курсы для backend-разработчиков, желающих повысить свои навыки в асинхронной веб-разработке. Всю информацию о всех школах можно найти на сайте, а также в нашем телеграм-чате.

ссылка на оригинал статьи https://habr.com/ru/company/kts/blog/560058/

Инициализация Rx цепочки

Всем привет, меня зовут Иван, я Android-разработчик. Сегодня хочу поделиться своим опытом работы с RxJava2 и рассказать, как происходит инициализация цепочки. Почему я вообще решил поднять эту тему? Пообщавшись со знакомыми разработчиками, я понял, что не каждый кто использует этот инструмент понимает, как он работает. И тогда я решил разобраться как же устроены подписки в RxJava2 и в какой последовательности вся работа инициализируется. Я не нашел ни одной статьи, поясняющей это. В свете этого я полез в исходники, чтобы посмотреть, как же все работает и набросал для себя небольшую шпаргалку, которая выросла в данную статью.

В этой статье я не буду описывать что такое Observable, Observer и все остальные сущности, которые используются в RxJava2. Если вы решили прочитать данную статью, то я предполагаю, что вы уже знакомы с этой информацией. А если вы всё же не знакомы с этими понятия, то я рекомендую перед началом чтения ознакомиться с ними.

Вот с чего можно начать:

Грокаем* RxJava

Исследуем RxJava 2 для Android

Давайте посмотрим, как работает простейшая цепочка:

Observable.just (1, 2, 3, 4, 5) .map {…} .filter {…} .subscribe();

По верхам

Сначала опишу вкратце каждый шаг, через который пройдем в этой цепочке (шаги начинаются сверху вниз):

  • Создается объект в операторе just ObservableFromArray.

  • Создается объект в операторе map ObservableMap, который принимает в конструктор ссылку на ранее созданный объект в операторе just.

  • Создается объект в операторе filter ObservableFilter, который принимает в конструктор ссылку на ранее созданный объект в map, в котором уже хранится ссылка на just.

  • После создания всех Observable’ов у последнего Observable в цепочки вызывается метод subscribe() (в нашем случае это ObservableFilter созданный в операторе filter) в котором создается новый Observer, который и будет обрабатывать все полученные события.

  • В методе ObservableFilter.subscribe() вызывается следующий метод ObservableFilter.subscribeActual(), в котором создается внутренний Observer, в случае с оператором filter, это FilterObserver. В этот внутренний Observer передается ссылка на первый созданный Observer в ObservableFilter.subscribe().

  • Вызывается ObservableMap.subscribe() в котором так же вызывается ObservableMap.subscribeActual()и создается внутренний Observer, в случае с оператором map, это MapObserver, в который передается ссылка на FilterObserver.

  • Вызывается ObservableFromArray.subscribe() и после ObservableFromArray.subscribeActual(), и уже там вызывается метод onSubscribe()у переданного в ObservableFromArray.subscribeActual() Observer’а.

  • onSubscribe() вызывается у каждого нижележащего Observer’а в цепочке.

  • ObservableFromArray начинает излучать все события в метод onNext() нижележащего Observer’а.

Визуальное представление описанной выше схемы.
Визуальное представление описанной выше схемы.

Создание источников данных

Теперь давайте рассмотрим описанные выше шаги подробнее, сначала попадаем в метод just() где происходит проверка каждого значения на null, далее идет вызов метода fromArray(), который возвращает Observable.

public static <T> Observable<T> just(T item1, T item2, T item3, T item4, T item5) {    ObjectHelper.requireNonNull(item1, "item1 is null");    ObjectHelper.requireNonNull(item2, "item2 is null");    ObjectHelper.requireNonNull(item3, "item3 is null");    ObjectHelper.requireNonNull(item4, "item4 is null");    ObjectHelper.requireNonNull(item5, "item5 is null");     return fromArray(item1, item2, item3, item4, item5); }

В fromArray() проверяется, что метод принимает в себя не пустой массив и имеет больше одного элемента.

public static <T> Observable<T> fromArray(T... items) {    ObjectHelper.requireNonNull(items, "items is null");    if (items.length == 0) {        return empty();    }    if (items.length == 1) {        return just(items[0]);    }    return RxJavaPlugins.onAssembly(new ObservableFromArray<T>(items)); }

После прохода через все эти шаги создается новый экземпляр класса ObservableFromArray, который на вход принимает массив с данными.

После этого новый объект передается в метод onAssembly(), так как нет необходимости как-то еще модифицировать Observable, то возвращается тот же источник, который поступал на вход.

public static <T> Observable<T> onAssembly(@NonNull Observable<T> source) {    Function<? super Observable, ? extends Observable> f = onObservableAssembly;    if (f != null) {        return apply(f, source);    }    return source; }

onAssembly() проверяет хотим ли перехватить текущий Observable и как-то модифицировать его, например таким образом:

RxJavaPlugins.setOnObservableAssembly(o -> { 	if (o instanceof ObservableFromArray) {     	return new ObservableFromArray<>(new Integer[] { 4, 5, 6 }); 	} 	return o; });   Observable.just(1, 2, 3) .filter(v -> v > 3) .test() .assertResult(4, 5, 6);
Только что созданный ObservableFromArray
Только что созданный ObservableFromArray

Вызывается следующий оператор в цепочке map(). Оператор проходит почти через те же шаги, что были описаны выше. Сначала делается проверка на null, потом создается новый экземпляр класса ObservableMap.

public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {    ObjectHelper.requireNonNull(mapper, "mapper is null");    return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper)); }

Вот тут появляется небольшое отличие, в конструктор ObservableMap передается не только mapper, который будет преобразовывать одно значение в другое, но также принимает в конструктор this (source). В данном случае this это ссылка на экземпляр класса ObservableFromArray созданный на предыдущем шаге. ObservableMap расширяет абстрактный класс AbstractObservableWithUpstream, в котором и храниться source.

AbstractObservableWithUpstream абстрактный класс, который реализуют Observable операторы и в котором хранится ссылка на вышележащий источник данных.

Далее происходит вызов метода onAssembly() и возвращение созданного Observable

Обновленная схема с созданным ObservableMap
Обновленная схема с созданным ObservableMap

Переходим к следующему оператору в цепочки filter(). В нем не происходит ничего нового, за исключением того, что создается объект ObservableFilter и в его конструктор в this передается ссылка на экземпляр ObservableMap (у которого уже есть ссылка на ObservableFromArray, как показано на схеме выше) созданный на предыдущем шаге.

public final Observable<T> filter(Predicate<? super T> predicate) {    ObjectHelper.requireNonNull(predicate, "predicate is null");    return RxJavaPlugins.onAssembly(new ObservableFilter<T>(this, predicate)); }
Обновленная схема с созданным ObservableFilter
Обновленная схема с созданным ObservableFilter

Начало подписки

Последний оператор в цепочке subscribe(), который вызывает перегруженную версию метода. В нашем случае обрабатывается только onNext(). Метод subscribe() вызывается у ObservableFilter, который был последним созданным Observable в цепочке.

public final Disposable subscribe(Consumer<? super T> onNext) {    return subscribe(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION, Functions.emptyConsumer()); }

В перегруженном методе сначала проверяются все переданные параметры на null, далее создается объект класса LambdaObserver и происходит подписка.

public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,        Action onComplete, Consumer<? super Disposable> onSubscribe) {    ObjectHelper.requireNonNull(onNext, "onNext is null");    ObjectHelper.requireNonNull(onError, "onError is null");    ObjectHelper.requireNonNull(onComplete, "onComplete is null");    ObjectHelper.requireNonNull(onSubscribe, "onSubscribe is null");     LambdaObserver<T> ls = new LambdaObserver<T>(onNext, onError, onComplete, onSubscribe);     subscribe(ls);     return ls; }

А вот и сам метод, в котором и происходит подписка.

public final void subscribe(Observer<? super T> observer) {    ObjectHelper.requireNonNull(observer, "observer is null");    try {        observer = RxJavaPlugins.onSubscribe(this, observer);         ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");         subscribeActual(observer);    } catch (NullPointerException e) {       ......  } }

В методе subscribeActual() производится подписка на источник данных и в него же передается созданный ранее LambdaObserver. subscribeActual() вызывается в классе ObservableFilter. И вот что там происходит.

public void subscribeActual(Observer<? super T> observer) {    source.subscribe(new FilterObserver<T>(observer, predicate)); }

Создается новый объект класса FilterObserver, который принимает в конструктор LambdaObserver созданный ранее и предикат для фильтрации, которые хранится в ObservableFilter в виде поля класса.

Класс FilterObserver расширяет класс BasicFuseableObserver, в котором уже реализован метод onSubscribe(). BasicFuseableObserver это абстрактный класс, который реализуют промежуточные Observer’ы. Если посмотреть исходники, то его реализуют только 6 классов, два из которых это FilterObserver и MapObserver. В методе BasicFuseableObserver.onSubscribe() также вызывается метод onSubscribe() у нижележащего Observer’а, который передавался в конструктор этого класса. А выглядит это вот так:

public final void onSubscribe(Disposable d) {    if (DisposableHelper.validate(this.upstream, d)) {        this.upstream = d;        if (d instanceof QueueDisposable) {            this.qd = (QueueDisposable<T>)d;        }        if (beforeDownstream()) {             downstream.onSubscribe(this);             afterDownstream();        }    } }

После того, как подписались на ObservableFilter и создали объект FilterObserver, он передается в source.subscribe(). Хочу напомнить, что source это объект класса ObservableMap, переданный ранее в цепочке. У объекта ObservableMap вызывается метод subscribe().

public final void subscribe(Observer<? super T> observer) {    ObjectHelper.requireNonNull(observer, "observer is null");    try {        observer = RxJavaPlugins.onSubscribe(this, observer);         ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");         subscribeActual(observer);    } catch (NullPointerException e) {       ......  } }

Далее происходит те же шаги, в методе subscribe() вызывается subscribeActual(), оба этих метода вызываются у ObservableMap. В subscribeActual() создается новый MapObserver с переданным в качестве параметра экземпляром FilterObserver и функцией mapper’а. 

public void subscribeActual(Observer<? super U> t) {    source.subscribe(new MapObserver<T, U>(t, function)); }
public void subscribeActual(Observer<? super T> observer) {    FromArrayDisposable<T> d = new FromArrayDisposable<T>(observer, array);     observer.onSubscribe(d);     if (d.fusionMode) {        return;    }    d.run(); }

Все рассмотренные Observer’ы расширяли абстрактный класс BasicFuseableObserver, в котором уже реализован метод onSubscribe() и, так же есть ссылка на нижележащий Observer, у которого так же вызывается метод onSubscribe().

В конце метода subscribeActual() вызывается метод run(), в котором и начинается излучение всех данных в нижележащие Observer’ы.

void run() {    T[] a = array;    int n = a.length;     for (int i = 0; i < n && !isDisposed(); i++) {        T value = a[i];        if (value == null) {            downstream.onError(new NullPointerException("The element at index " + i + " is null"));            return;        }        downstream.onNext(value);    }    if (!isDisposed()) {        downstream.onComplete();    } }

Соответственно вызываются onNext() для передачи значений в нижележащие Observer’ы, и потом onComplete() при завершении излучения данных или может произойти ошибка и вызовется onError(), который завершит всю цепочку.

Визуальное представление процесса создания и подписок
Визуальное представление процесса создания и подписок

Вывод

Observable’ы вкладываются друг в друга и вызывают callback’и для создания Observer’ов, которые и будут обрабатывать получаемые данные и передавать их дальше по цепочки.

Метод onSubscribe() вызывается до начала отправки данных и это надо иметь ввиду если вы пользуетесь такими оператора, как doOnSubscribe().

На каждый оператор создается как минимум 3 объекта:

  • Анонимный класс передаваемый в оператор

  • Observable создаваемый внутри оператора

  • Observer обрабатывающий получаемые данные

Потому при использовании операторов стоит иметь ввиду, что каждый оператор аллоцирует память для несколько объектов и не стоит добавлять операторы в цепочку, только потому что “можно”.

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

ссылка на оригинал статьи https://habr.com/ru/post/560162/

Intel 5G Solution 5000 — 5G M.2 модем для ноутбуков

На проходящей в эти дни выставке Computex 2021 компания Intel представила 5G Solution 5000 — первый в ее истории 5G модем в форм-факторе М.2. Новый продукт стал результатом сотрудничества трех компаний: Intel — разработчика ПО для хост-системы, MediaTek, ответственной за модем и Fibocom, соединившей все компоненты в готовое устройство FM350-GL.

Напомним, что в 2017-18 годах Intel активно развивала мобильное направление и выпустила несколько моделей 5G контроллеров, таких как XMM 8060 (первый модем компании) и XMM 8160, однако далее она отказалась от роли самостоятельного игрока на этом рынке, продав в июле 2019 уже имеющиеся наработки компании Apple. Ставка была сделана на стратегическое сотрудничество с третьими компаниями, и уже в конце 2019 был оформлен альянс с одним из крупнейших мировых чипмейкеров тайваньской компанией MediaTek.

Модем представляет собой PCIe устройство в форм-факторе М.2. Обращает на себя внимание нестандартный для М.2 размер карточки — 30×52 мм, что потребует специального дизайна отсека. Intel 5G Solution 5000 планируется устанавливать в ноутбуки на базе процессоров Intel Tiger Lake и Alder Lake, основные его характеристики приведены в таблице.

Стандарты связи 5G NR/ 4G LTE/ 3G WCDMA
Форм-фактор M2, 30×52 односторонний
Диапазон частот Sub 6GHz
Скорость (DL/UL) 4.7 Гбит/c / 1.25 Гбит/c в режиме 5G
SIM Встроенная eSIM
Поддержка ОС Windows, Chrome, Linux

ссылка на оригинал статьи https://habr.com/ru/company/intel/blog/560124/

Использование TLS fingerprinting для выявления угроз

В статье хотим рассказать про технологию TLS fingerprinting, про которую недостаточно материалов в русскоязычном сегменте. Попробуем это исправить. Статья частично переводит тематические материалы авторов описываемых методов (тут и тут), а также содержит описание практической реализации от Акрибии.

Не будем глубоко погружаться в детали работы SSL/TLS (далее будем говорить TLS), но кратко поясним детали.

Использование TLS само по себе благо, так как с его помощью шифруются данные. Но с обратной стороны создатели вредоносов используют его же, чтобы скрываться в шифрованном трафике (в данной статье как раз будет уклон в эту сторону) и затруднять их обнаружение и нейтрализацию.

Чтобы инициировать сеанс TLS, клиент отправляет «пакет» приветствия серверу после трёхстороннего установления связи TCP. Этот «пакет» и способ его создания зависят от пакетов и методов шифрования, используемых при создании клиентского приложения. Если сервер принимает соединение TLS, он ответит пакетом приветствия, тем самым продолжая согласование шифрования.

Поскольку согласование шифрования TLS передаётся в открытом виде, то можно отследить и идентифицировать клиентские приложения.

В этом и суть технологии TLS fingerprinting, если кратко. А теперь немного подробнее.

TLS fingerprinting

Суть технологии в захвате элементов пакета приветствия клиента, которые остаются статичными от сеанса к сеансу для каждого клиента. На их основе можно создать «отпечаток пальца» для распознавания конкретного клиента в последующих сеансах. Записываются следующие поля:

  • версия TLS;

  • версия записи TLS;

  • наборы шифров;

  • параметры сжатия;

  • список расширений.

Кроме того, данные собираются из трех конкретных расширений (если они доступны):

  • алгоритмы подписи;

  • алгоритм для шифрования данных;

  • хэш функция для проверки содержимого.   

Использование такого набора данных позволяет с большой точностью идентифицировать используемый клиент (например, браузер).

Почему это круто:

  • Пакет приветствия клиента — это первый пакет в любом TLS-соединении. Это позволяет принимать решение о последующих мерах в начале сеанса до того, как потребуется подмена протокола или эмуляция.

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

  • Пакеты приветствия клиента возникают достаточно редко, поэтому дополнительная обработка хэшей не повлечет значительных затрат.     

Наиболее очевидное использование TLS Fingerprinting – пассивное обнаружение. Технология позволяет обнаруживать широкий спектр потенциально нежелательного трафика, не требуя доступа к конечным точкам. Можно обнаруживать как конкретные вредоносы по их поведению и/или обращениям к командным центрам, так и обычный софт, используемый не по назначению или в обход действующих правил.

Например, к серверу Exchange могут обращаться только почтовые клиенты или браузер, если используется OWA, поэтому соединение из скрипта на Python будет подозрительным.

Итого: TLS Fingerprinting предназначен для быстрой идентификации известных TLS-соединений и отслеживания неизвестных TLS-соединений. Входные данные принимаются либо посредством прослушивания трафика, либо при чтении PCAP файлов.

Есть несколько готовых реализаций, наиболее поддерживаемых комьюнити:

  • пассивный метод с использованием хэшей JA3 и JA3S;

  • активный инструмент снятия отпечатков пальцев с сервера TLS – хэши JARM.

JA3 и JA3S

Метод JA3 используется для сбора десятичных значений байтов для следующих полей в пакете приветствия клиента: версия TLS, набор шифров, список расширений протоколов TLS, эллиптические кривые и форматы эллиптических кривых. Затем он объединяет эти значения вместе по порядку, используя символ «,» для разграничения каждого поля и «-» для разграничения каждого значения в каждом поле.

Порядок полей следующий:

TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats

Пример:

771,49196-49162-49195-52393-49161-49200-49172-49199-52392-49171-159-57-56-107-158-52394-51-50-103-22-19-157-53-61-156-47-60-10,0-23-65281-10-11-13-28,29-23-24-25,0

Если в ClientHello нет расширений TLS, поля остаются пустыми:

769,4–5–10–9–100–98–3–6–19–18–99,,,

Эти строки затем хэшируются MD5. Пример отпечатка JA3:

c8446f59cca2149cb5f56ced4b448c8d

JA3S – это хэш идентификации сервера. Метод JA3S заключается в сборе десятичных значений байтов для следующих полей в пакете приветствия сервера: версия TLS, набор шифров и список расширений протоколов TLS. После сбора значений, происходит процесс объединения этих значений вместе по порядку, используя «,» для разграничения каждого поля и «-» для разграничения каждого значения в каждом поле.

Порядок полей, следующий:

TLSVersion,Cipher,Extensions

Пример:

769,47,65281–0–11–35–5–16

Если в Server Hello нет расширений TLS, поля остаются пустыми.

Пример:

769,47,

Эти строки затем хэшируются MD5 для создания 32-символьного отпечатка пальца. 

Это отпечаток JA3S:

4835b19f14997673071435cb321f5445

JA3 и JA3S – это методы снятия отпечатков TLS. JA3 отслеживает способ, которым клиентское приложение обменивается данными через TLS, а JA3S отслеживает ответ сервера. Вместе они создают отпечаток криптографического согласования между клиентом и сервером.

Теперь перейдем к описанию активного метода JARM.

JARM

JARM работает, активно отправляя 10 пакетов приветствия клиента TLS на целевой сервер и фиксируя определенные атрибуты ответов приветствия сервера. Затем агрегированные ответы сервера TLS хэшируются определенным образом для получения отпечатка JARM. JARM отправляет разные версии, шифры и расширения TLS в разном порядке для сбора уникальных ответов. Хэш JARM представляет собой гибридный нечеткий хэш, он использует комбинацию обратимого и необратимого алгоритма хеширования для создания 62-символьного отпечатка.

Отпечатки JARM можно использовать:

  • убедиться, что все серверы в группе имеют одинаковую конфигурацию TLS;

  • сгруппировать разрозненные серверы в сети Интернет по конфигурации, указав, например, что сервер может принадлежать Google, Yandex или Apple;

  • определить приложения или инфраструктуру по умолчанию;

  • выявить командные центры и других вредоносные серверы в сети Интернет.

Первые 30 символов состоят из шифра и версии TLS, выбранных сервером для каждого из 10 отправленных приветствий клиента. «000» означает, что сервер отказался согласовывать приветствие с этим клиентом. Остальные 32 символа представляют собой усеченный хэш SHA256 совокупных расширений, отправленных сервером, без учета данных сертификата x509. При сравнении отпечатков JARM, если первые 30 символов совпадают, но последние 32 отличаются, это будет означать, что серверы имеют очень похожие конфигурации, принимают одинаковые версии и шифры, но используют различные расширения, что не позволяет их считать полностью идентичными.

Исторически так сложилось, что индустрия кибербезопасности была сосредоточена в основном на индикаторах компрометации (IOC) и индикаторах атаки (IOA). То есть когда вредоносное ПО/хост и т.п. будут кем-то обнаружены, исследователи или вендоры платформ TI вычленяют уникальные или не очень признаки выявленного вредоноса или атаки типа IP, домена, хэша файла и т.п. и публикуют их в «чёрных списках». Проблема в том, что к этому моменту вредоносное ПО уже распространено, и специалисты ИБ автоматически переходят в оборону.

Интернет-сканирование JARM в сочетании с другими метаданными и историческим анализом даёт возможность упреждающей идентификации IOC для новых вредоносов. Например, можно сканировать Интернет с помощью JARM, сопоставлять известные результаты JARM с историей домена, IP и репутацией вместе с данными сертификата для создания черного списка. Это позволяет перейти к возможности программного создания высокоточных списков блокировки до того, как первая вредоносная программа будет распространена.

Можно использовать JARM автоматически сканируя все хосты назначения, детектируемые в сети компании, для обогащения событий, и использовать сводную таблицу, чтобы не сканировать одни и те же хосты несколько раз. Затем можно запускать запросы заведомо плохих JARM или использовать список для корреляции.

Чтобы упростить процесс, можно использовать готовое решение. В настоящее время хэши JARM умеют считать продукты Palo Alto Networks  и используют их API для обогащения целевого JARM.

Примеры реализации

Если нет возможности или желания использовать готовые решения от Palo Alto и пр., можно реализовать в своей инфраструктуре свое средство мониторинга трафика, например, на основе Zeek (ранее назывался Bro) – популярного open-source продукта, заточенного специально для этого.

Zeek собирает огромное количество информации из первоначального согласования TLS, т.к. оно обрабатывается в открытом виде. Таким образом, хотя мы не можем видеть всё, что связано с шифрованием TLS, мы всё же можем получить общее представление о том, что происходит.

Zeek умеет выполнять снятие отпечатков TLS с помощью хэша JA3\JA3S.

Если у нас уже есть Zeek, в него попадает трафик, то для полноценного мониторинга нам понадобится SIEM (можно обойтись и без SIEM, но тогда обрабатывать логи Zeek’а придется вручную). Допустим, SIEM у нас все же есть. Помимо инструментов обработки трафика и событий Zeek нам еще потребуются списки готовых хэшей, чтобы было с чем сравнивать.

В случае в JA3 – все просто. Есть готовый инструмент, к которому можно обращаться по API для проверки выявленного хэша JA3 и принятия решения – нормально это или не очень.

Хэши JA3S, соответствующие вредоносам, достать чуть сложнее. Есть, например, небольшая подборка тут, есть и еще, нужно только искать или считать самостоятельно.

С хешами JARM еще немного сложнее, если у вас нет Palo Alto, их нужно собирать по разрозненным отчетам аналитиков или считать самостоятельно и вести собственные белые и черные списки. Но на github тоже попадаются тематические подборки, например, эта. Мы ее задействуем в дальнейшем при проработке правил по JARM.

Далее приведем некоторые описания правил, которые мы реализуем у себя. Также есть неплохой доклад от Splunk с примерами алгоритмов и правил мониторинга по хэшам JA3/JA3S. Доступен здесь. Правила можно адаптировать под любую SIEM.

  • Выявление признака конкретного вредоноса по хэшам JA3\JA3S. Вот, например, готовые значения для Emotet и TrickBot:

    JA3 = 4d7a28d6f2263ed61de88ca66eb011e3 (Emotet) JA3S = 80b3a14bccc8598a1f3bbe83e71f735f (C2 Server Response) JA3 = 6734f37431670b3ab4292b8f60f29984 (Trickbot) JA3S = 623de93db17d313345d7ea481e7443cf(C2 Server Response)
  • Выявления нового хэша JA3, ранее не появляющегося в сети.

Список клиентского ПО в любой сети, как правило, достаточно статичен, и, если появится что-то новенькое – это однозначно повод разобраться. Правда, это может быть новая версия уже известного ПО, тогда ее нужно добавить в белый список.

  • Выявления хэшей JA3 не из белого списка.

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

  • Выявления хэшей JA3\JA3S из чёрного списка.

Это правило нацелено на выявление хэшей заведомо вредоносного ПО.

  • Выявление обращений к C&C по хэшам JARM.

При выявлении обращения к TLS-серверу, неизвестному нам или ранее не появляющемуся в логах, необходимо посчитать его JARM хэш (можно вручную, можно скриптом или с использованием SOAR), при совпадении с хэшем, соответствующим известному C&C серверу, блокируем соединение, изолируем хост и расследуем инцидент.

  • Выявление JA3 хэшей, не характерных для системы.

При появлении на хостах с Windows JA3 хэшей, характерных для Linux или смартфонов (Android/IOS), стоит обратить внимание на такие хосты. Это может являться признаком работы вредоносов (ну или запущенного эмулятора/виртуалки в режиме NAT). Кейс особенно заслуживает внимание, если выявлен на рабочей станции обычного пользователя, далекого от мира IT.

  • Выявление JA3 хэшей, характерных для разных браузеров одного семейства.

Сейчас достаточно сложно на один хост поставить две разные версии Firefox или Chrome (виртуальные машины в режиме NAT не в счёт). Такое выявление может свидетельствовать о том, что злоумышленники пытались адаптироваться под установленный софт, но после обновления софта не успели или забыли обновить свой Fingerprint. Хост лучше изолировать и провести расследование.

  • Выявление JA3 хэшей, характерных для популярных сетевых библиотек языков программирования.

Ни для кого не секрет, что сейчас ВПО пишется не только на C/C++. Часто встречаются образцы, которые написаны на Python или Golang. Стандартные библиотеки, такие как requests (для python) или http (для Golang), имеют характерные отпечатки в рамках нескольких версий. В случае выявления такого хэша на рабочем месте разработчиков, вероятно, это нормально. Но, если хэши «всплывут» на рабочем компьютере рядового пользователя, то точно следует провести тщательную проверку, так как с большой вероятностью это будет свидетельством активности вредоноса. Также стоит поддерживать списки актуальных JA3 хэшей для популярных сетевых библиотек, чтобы минимизировать ложные срабатывания.

Важно: Список хэшей JARM (и JA3S тоже) необходимо регулярно пополнять новыми выявленными C&C серверами, полученную самостоятельно или от сторонних исследователей.

При использовании средств защиты сети, которые умеют вычислять хэши JARM самостоятельно и на лету, соединения с серверами из черных списков можно и нужно блокировать автоматически.


В завершении хотим подчеркнуть, что развитие и популяризация технологии TLS Fingerprinting, по нашему мнению, совсем не за горами, и способствует этому внедрение TLS 1.3.

В версиях стандарта TLS до 1.3 у клиента была возможность сообщать, к какому серверу он хочет обратиться — SNI (Server Name Indication). Чаще всего в HTTPS для этого использовалось поле HOST, чтобы на одном IP и порту могли работать несколько HTTPS-сайтов. Соответственно и так, без всяких fingerprint, было видно, кто куда идет. Понятно, что речь идёт про легитимные обращения, атакующие всегда могли подделать SNI.

В стандарте TLS 1.3 появилась возможность шифровать имя запрашиваемого сайта – Encrypted SNI (ESNI), и безопасники потеряли возможность видеть, куда идёт обращение.

Правда ESNI уже запретили в Китае, и были попытки блокировок в России. Однако ESNI стал часто встречаться, в том числе в инструментах злоумышленников, и без TLS fingerprinting становится сложно понимать, куда происходят обращения в сети.

Авторы статьи:

Михаил Кравченко, SOC-аналитик;

Александр Минин, руководитель направления Threat Intelligence  @AAMinin;

Анна Шестакова, руководитель направления мониторинга ИБ.

ссылка на оригинал статьи https://habr.com/ru/company/acribia/blog/560168/

Microsoft разработала ИИ-систему, преобразующую простые текстовые запросы в код

image

Корпорация Microsoft на конференции для разработчиков Microsoft Build, которая прошла несколько дней назад, представила интересную новинку. Это модель обработки естественного языка GPT-3 от OpenAI, позволяющая создавать приложения с минимальным знанием языков программирования.

Да, здесь открывается широкий простор для шуток, но у этой разработки неплохие перспективы. Модель преобразует запросы на обычном языке — пока что лишь английском, в код. Результат преобразования не слишком сложный, все же нейросеть не в состоянии написать код на несколько тысяч строк по парочке обычных вопросов. Но потенциал у разработки есть, и немалый.

Обычный текст в код? Так разве бывает?

В целом, да, нейросеть способна преобразовать не самые сложные вопросы вроде «найти товары, название которых начинается со слова kids». Система обрабатывает такой запрос без проблем, выдавая результат вида «Filter(‘BC Orders’ Left(‘Product Name’,4)=»Kids")".

Пока что нейросеть работает лишь с одним ЯП — языке с открытым исходным кодом Power FX, основанном на Excel. Как и говорилось выше, система распознает лишь один язык — английский. Разработчики обещают, что дальше будет больше языков.

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

image

В Microsoft прокомментировали новую разработку следующим образом: «Сейчас на цифровые решения — огромный спрос, но программистов не хватает. Дефицит специалистов лишь в США составляет около 1 млн человек. Почему бы и не научить среду разработки говорить на обычном языке людей?».

Ну а теперь — подробности

В Microsoft идея подобного проекта возникла во время работы над Power Platform — это специализированная платформа, предназначенная для корпоративных клиентов. Платформа позволяла и позволяет решать базовые задачи партнеров корпорации без привлечения профессиональных разработчиков. Power Platform может оказаться крайне полезной для компаний, которые не могут по какой-то причине нанять опытных программистов. Решать можно такие задачи, как базовая аналитика, визуализация данных, автоматизация рабочих процессов.

Возможности платформы были показаны, в частности, на примере приложения, созданного для Coca-Cola. Это приложение ведет мониторинг поставок концентрата колы на разные объекты. Интерфейс приложения создается в визуальном конструкторе, напоминающем Power Point. Правда, для создания меню, которое позволяет пользователям взаимодействовать с базой данных, нужно базовое знание формул Microsoft Power Fx.

image

И здесь обычный пользователь получает проблему — ведь далеко не все разбираются в этих формулах. Понятно, что профессиональный программист сделает все быстро, но, как говорилось выше, не все компании могут вовремя найти разработчика, а у кого-то просто нет на программиста денег.

В общем, для того, чтобы облегчить жизнь своим клиентам, корпорация Microsoft разработала алгоритм, который «слушает» пользователя, «понимает» его и составляет необходимую формулу. Так, например, вместо того, чтобы пользователь выполнял поиск в базе данных с запросом «FirstN (Sort (Search (‘BC Orders’,« Super_Fizzy »,« aib_productname »),« Purchase Date ‘, Descending), 10) », он может просто написать «Покажите 10 заказов, в названии которых есть Super Fizzy, и отсортируйте их по дате покупки, указав самые новые вверху», и GPT-3 выдаст правильный код.

Не сказать, что алгоритм супер-сложный или сверх-современный, но это то, что может сэкономить время и деньги миллионов пользователей. Пока что система тестируется, для пользователей она станет доступна начиная с июня (превью-версия). В целом, Microsoft не первая компания, предложившая подобную идею. Но ее реализация до крайности проста и эффективна. Для того, чтобы снизить вероятность ошибки, пользователю дается возможность подкорректировать финальную формулу. Кроме снижения количества ошибок, это еще и возможность постепенного обучения пользователя — рано или поздно даже далекий от IT человек начнет понимать что к чему.

Перспективы технологии

Сейчас Microsoft разработала безопасную реализацию GPT-3. Безопасную в том смысле, что если пользователь и допустит ошибку (либо ошибется сам алгоритм), это никому ничем не грозит. Ошибку, скорее всего, найдут и поправят.

А вот реализация других сервисов на основе GPT-3 бывает и несколько опасной. Например, медицинский сервис, экспериментальный чат-бот, который общается с пациентами и дает им медицинские советы, как-то в ходе разговора рекомендовал одному из собеседников убить себя. И это не шутка. Хорошо, что сервис только тестировался — ведь подобный совет, который увидит тяжело больной человека может быть понят и воспринят буквально.

Для того, чтобы снизить количество возможных проблем, корпорация Microsoft приняла меры. В частности, создан список запретных слов и фраз, на которые система не реагирует. «Если вы попытаетесь заставить ИИ генерировать код для реализации плохой идеи, то у вас ничего не получится. Если вдруг сервис ошибочно не будет реализовать запрос, всегда можно обратиться в службу поддержки», — заявил один из разработчиков платформы. Под «плохими» идеями подразумевается нечто вроде фильтрации пользователей по цвету кожи, религии или национальности.

В целом, это только начало реализации масштабных планов Microsoft. В ближайшем будущем генерация формул по запросу голосом может быть включена в функционал Excel. Пока что можно говорить о внедрении, полном или частичном, новой платформы в продукты, где нужна работа с Power Fx. В будущем, возможно, платформа станет еще более функциональной, так что разрабатывать простые приложения можно будет просто надиктовывая текст.

ссылка на оригинал статьи https://habr.com/ru/company/neuronet/blog/560182/