×

Взаємодія між Erlang/OTP та ASN.1. Як це працює

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Many programs don’t have a well-defined interface. They should have — Joe Armstrong

Усім привіти! З Вами ​Ukrainian Erlanger 🤘 У цій статті я хочу поговорити про Erlang/OTP і ASN.1, особливо про ASN.1 у Erlang. Варто зазначити, що метою цієї статті є не навчити ASN.1, а показати можливості та взаємодію між Erlang/OTP та ASN.1. Однак у статті ви знайдете перелік ресурсів для самостійного навчання та глибшого розуміння стандартів ASN.1 та багато іншого.

Також хочу звернути вашу увагу на те, що не кожна мова може підтримувати ASN.1 з коробки — але це не стосується Erlang/OTP, і це дивовижно! Оскільки кожен проєкт Erlang може використовувати цю чудову функціональність за замовчуванням.

So, let’s rock with it! 🤘

Що таке ASN.1? Згідно з Wiki:

Abstract Syntax Notation One (ASN.1) — це стандартна мова опису інтерфейсу для визначення структур даних, які можна серіалізувати та десеріалізувати міжплатформним способом.

Основні напрямки використання ASN.1 — це створення криптографічних програм і використання у галузі телекомунікацій. Звучить круто, правда? Але як саме Erlang працює з ASN.1, і як ми можемо застосувати його, використовуючи той самий ASN.1 для різних клієнтів, написаних іншими мовами? Давайте спробуємо у цьому розібратися!

Як саме Erlang працює з ASN.1

Erlang/OTP включає asn1, додаток, який збирає модулі під час компіляції для підтримки та виконання ASN.1. Давайте почнемо з простого прикладу, який ми можемо взяти з Wikipedia:

FooProtocol DEFINITIONS ::= BEGIN

   FooQuestion ::= SEQUENCE {
       trackingNumber INTEGER,
       question       IA5String
   }

   FooAnswer   ::= SEQUENCE {
       questionNumber INTEGER,
       answer         BOOLEAN
   }

END

Ми можемо зберегти це у файл під назвою FooProtocol.asn1. Ім’я файлу має мати те саме ім’я, яке було написано перед DEFINITIONS. У той же час розширення файлу може бути будь-яким, але для зручності я вибрав саме *.asn1.

Як бачите, цей модуль (як і інші подібні модулі ASN.1) відповідатиме такому шаблону скелета:

  • Ідентифікатор модуля: у нашому прикладі це FooProtocol.
  • Ключове слово DEFINITIONS.
  • Символ ::=
  • Тіло модуля, яке складатиметься з операторів експорту та імпорту, якщо такі є, за якими слідують присвоєння типу та значень, які укладені між BEGIN та END.

Звичайно, для глибокого розуміння ASN.1 або у випадку, якщо ви хочете дізнатися трохи більше про типи даних, класи, структури тощо, вам потрібно буде звернутися до джерел, перерахованих у центральній частині ASN.1 стандартів:

Standard

ISO

Description

X.680

ISO/IEC 8824-1

Basic ASN.1 Notation

X.681

ISO/IEC 8824-2

Information Objects Specification

X.682

ISO/IEC 8824-3

Constraint Specification

X.683

ISO/IEC 8824-4

Parameterization

X.690

ISO/IEC 8825-1

Basic Encoding Rules

X.691

ISO/IEC 8825-2

Packed Encoding Rules

Щоб зібрати модуль ASN.1 у Erlang/OTP, ви можете використовувати erlc у вашому терміналі:

$ erlc FooProtocol.asn1

Або ви можете скомпілювати модулі ASN.1 у оболонці Erlang наступним чином:

1> asn1ct:compile('FooProtocol.asn1'). %% or: "FooProtocol.asn1"

У той же час зверніть, будь ласка, вашу увагу на те, що rebar3 ще не підтримує ASN.1. Щоб зібрати модулі ASN.1 у проєкти rebar3, вам потрібно буде знайти та використовувати існуючі плагіни або створити власний rebar3 plugin 😜

Тепер давайте спробуємо скомпілювати наш модуль за допомогою Erlang та подивимося, які файли було згенеровано після компіляції:

$ erlc FooProtocol.asn1
$ tree -a
.
├── FooProtocol.asn
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl0 directories, 5 files

Як бачите, Erlang/OTP під час компіляції створив чотири додаткові файли на основі нашого простого зразкового ASN.1 модуля. А саме:

  • FooProtocol.erl — модуль з функціями кодування, декодування та значення.
  • FooProtocol.hrl — структури записів (records) на основі визначень SEQUENCE.
  • FooProtocol.asn1db — файл проміжного формату, який може використовуватися компілятором, коли в інших модулях є визначення IMPORT.
  • FooProtocol.beam — скомпільований модуль.

Варто знати, що ви можете вибрати будь-які правила кодування. Але якщо правило кодування опущено, за замовчуванням буде використовуватися BER (Basic Encoding Rules).

Давайте трохи пограємо з цими модулями в оболонці Erlang:

$ erl
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Eshell V12.0.3  (abort with ^G)

1> rr("FooProtocol.hrl").
['FooAnswer','FooQuestion']

2> {ok, EncQuestionBin} =
 'FooProtocol':encode('FooQuestion',
                       #'FooQuestion'{
                           trackingNumber = 1,
                           question = "Do you understand?"}).
{ok,<<48,23,2,1,1,22,18,68,111,32,121,111,117,32,117,110,
     100,101,114,115,116,97,110,100,63>>}

3> {ok, EncAnswerBin} =
'FooProtocol':encode('FooAnswer',
                     #'FooAnswer'{
                         questionNumber = 1,
                         answer = true}).
{ok,<<48,6,2,1,1,1,1,255>>}

4> 'FooProtocol':decode('FooQuestion', EncQuestionBin).
{ok,#'FooQuestion'{trackingNumber = 1,
                  question = "Do you understand?"}

5> 'FooProtocol':decode('FooAnswer', EncAnswerBin).
{ok,#'FooAnswer'{questionNumber = 1,answer = true}}

Виглядає досить захоплююче!

Як ми можемо застосувати ці знання, використовуючи той самий ASN.1

Ось думка: чи можемо ми використовувати той самий модуль ASN.1, але для різних мов? Це звучить, як хороший виклик. Для сервера я виберу Erlang/OTP. Для клієнта я виберу Python3. Давайте створимо простий сервер UDP на Erlang і простий UDP-клієнт на Python.

UDP-сервер Erlang

Модуль ASN.1 — FooProtocol.asn1:

FooProtocol DEFINITIONS ::= BEGIN

   FooQuestion ::= SEQUENCE {
       trackingNumber INTEGER,
       question       IA5String
   }

   FooAnswer   ::= SEQUENCE {
       questionNumber INTEGER,
       answer         BOOLEAN
   }

END

Наш UDP-сервер має бути простим, як і наша модель ASN.1. Відкриваємо порт UDP. Тоді ми продовжуємо його слухати. Коли ми отримуємо повідомлення по UDP, ми перевіряємо, що воно має структуру, визначену в нашому модулі ASN.1, і ми можемо очікувати, що наш клієнт Python 3 надішле нам запитання: «Do you understand?».

Якщо це повідомлення збігається з нашим pattern matching, то у відповідь ми повинні надіслати повідомлення із значенням true та з таким же номером ідентифікатора, який є у самому тілі запиту, закодованого нашим модулем ASN.1 (звичайно). Для інших невідповідних запитань ми повернемо ту саму відповідь, але зі значенням false, таким чином даючи клієнту знати, що ми не розуміємо його запитання 😃

UDP-сервер Erlang/OTP — asn_server.erl:

module(asn_server).

-export([start/0]).

-include("FooProtocol.hrl").

start() ->
  spawn(fun() -> server(8181) end).

server(Port) ->
  {ok, Socket} = gen_udp:open(Port, [binary, {active, false}]),
  loop(Socket).

loop(Socket) ->
   inet:setopts(Socket, [{active, once}]),
   receive
       {udp, Socket, Host, Port, Bin} ->
           Answer = handle_message(Bin),
           gen_udp:send(Socket, Host, Port, Answer),
           loop(Socket)
   end.

handle_message(Bin) ->
   {ok, Dec} = 'FooProtocol':decode('FooQuestion', Bin),
   case Dec of
       #'FooQuestion'{trackingNumber = N,
                                question = "Do you understand?"} ->
           encode_answer(N, true);
       #'FooQuestion'{trackingNumber = N} ->
           encode_answer(N, false)
   end.

encode_answer(Number, Boolean) ->
   Rec = #'FooAnswer'{questionNumber = Number, answer = Boolean},
   {ok, FooAnswer} = 'FooProtocol':encode('FooAnswer', Rec),
   FooAnswer.

Скомпілюйте всі ці речі та запустіть UDP-сервер:

$ erlc FooProtocol.asn1 asn_server.erl
$ tree -a
.
├── asn_server.beam
├── asn_server.erl
├── FooProtocol.asn1
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl
$ erl
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Eshell V12.0.3  (abort with ^G)

1> asn_server:start().
<0.82.0>

Python UDP-клієнт

Як я вже згадував раніше, не всі мови програмування можуть підтримувати ASN.1 з коробки. Як ви вже могли здогадатися, Python є однією із них. Після деякого пошуку в Google я знайшов, на мою думку, певний захоплюючий інструмент: asn1tools. Оскільки я хочу працювати з Python 3, я встановив його за допомогою pip3:

$ sudo apt install python3-pip
$ pip3 install asn1tools

Тепер ми можемо спробувати створити наш UDP-клієнт на Python.

Наш клієнт Python також буде досить простим. Цей клієнт відкриє та прослухає сокет UDP; потім він згенерує одне правильне повідомлення та одне повідомлення з помилкою для нашого UDP-сервера на основі того самого модуля ASN.1. Він надсилатиме кожне повідомлення, потім намагатиметься отримати відповідні відповіді, декодувати їх на основі ASN.1 та роздруковувати результати в оболонку.

UDP-клієнт на Python 3 — asn_client.py:

#!/usr/bin/env python3

import socket
import asn1tools

UDP_IP = "127.0.0.1"
UDP_PORT = 8181

FOO = asn1tools.compile_files('FooProtocol.asn1')

MESSAGE = FOO.encode('FooQuestion', {'trackingNumber': 1, 'question': 'Do you understand?'})
ERROR   = FOO.encode('FooQuestion', {'trackingNumber': 2, 'question': 'Error?'})

print("UDP target IP: %s"   % UDP_IP)
print("UDP target port: %s" % UDP_PORT)
print("message: %s"         % MESSAGE)
print("error message: %s"   % ERROR)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(MESSAGE, (UDP_IP, UDP_PORT))

data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
answer = FOO.decode('FooAnswer', data)

print("received message: %s" % answer)

sock.sendto(ERROR, (UDP_IP, UDP_PORT))
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
answer = FOO.decode('FooAnswer', data)

print("received error: %s" % answer)

Звичайно, очікується, що всі наші файли будуть розміщені в одній папці. Тепер давайте спробуємо запустити наш клієнт UDP:

$ chmod 777 asn_client.py
$ tree -a
.
├── asn_client.py
├── asn_server.beam
├── asn_server.erl
├── FooProtocol.asn1
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl
$ ./asn_client.py
UDP target IP: 127.0.0.1
UDP target port: 8181
message: bytearray(b'0\x17\x02\x01\x01\x16\x12Do you understand?')
error message: bytearray(b'0\x0b\x02\x01\x02\x16\x06Error?')
received message: {'questionNumber': 1, 'answer': True}
received error:   {'questionNumber': 2, 'answer': False}

Дуже добре! Схоже, все працює так, як ми очікували! Як ви помітили, ми щойно змогли використати ту саму модель ASN.1 для простого способу декодування/кодування двійкових повідомлень по UDP у моделі клієнт/сервер, яка була написана різними мовами. Це не маленький подвиг! Але ми можемо зробити набагато більше.

Більше функцій Erlang для ASN.1

Надається багато додаткових функцій, але, звичайно, не всі вони добре задокументовані. Ось список із посиланнями на документацію або модулі додатку asn1, які варто переглянути:

asn1ct, asn1_db, asn1ct_check, asn1ct_constructed_ber_bin_v2, asn1ct_constructed_per, asn1ct_eval_ext, asn1ct_func, asn1ct_gen, sn1ct_gen_ber_bin_v2, asn1ct_gen_check, asn1ct_gen_jer, asn1ct_gen_per, asn1ct_imm, asn1ct_name, asn1ct_parser2, asn1ct_pretty_format, asn1ct_rtt, asn1ct_table, asn1ct_tok, asn1ct_value, asn1rt_nif.

Де використовується ASN.1

Наприклад, сам Erlang/OTP використовує ASN.1 принаймні для public_key і public_key_records. ASN.1 також можна використовувати для зашифрованного обміну повідомленнями у месенджерах або для виконання шифрування різних протоколів і, звичайно, у багатьох програмах у сфері телекомунікацій тощо. Але, звісно, вам вирішувати, у якому напрямку використовувати ASN.1, також ви можете знайти та відкрити йому нові можливості, наприклад, як ми зробили це у нашому простому UDP-прикладі вище 😎

Посилання:

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі