Взаємодія між 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 |
Basic ASN.1 Notation |
X.681 |
ISO/IEC |
Information Objects Specification |
X.682 |
ISO/IEC |
Constraint Specification |
X.683 |
ISO/IEC |
Parameterization |
X.690 |
ISO/IEC |
Basic Encoding Rules |
X.691 |
ISO/IEC |
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-прикладі вище 😎
Посилання:
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів