Men avtorizatsiya va S3 bilan o'zimning PyPI omborimni yaratdim. Nginx-da

Ushbu maqolada men Nginx Inc tomonidan ishlab chiqilgan Nginx uchun JavaScript tarjimoni NJS bilan o'z tajribamni baham ko'rmoqchiman va uning asosiy imkoniyatlarini haqiqiy misol yordamida tasvirlab bermoqchiman. NJS JavaScript-ning quyi to'plami bo'lib, u Nginx funksiyalarini kengaytirish imkonini beradi. Savolga nega o'z tarjimoningiz??? Dmitriy Volyntsev batafsil javob berdi. Qisqasi: NJS nginx yo'lidir va JavaScript Luadan farqli ravishda yanada progressiv, "mahalliy" va GCsiz.

Uzoq vaqt oldin…

Oxirgi ishimda men kaniko relslariga o'tkazilgan docker-compose, dind va boshqa lazzatlarga ega bir qancha rang-barang CI/CD quvurlari bilan gitlabni meros qilib oldim. Ilgari CIda ishlatilgan tasvirlar asl shaklida ko'chirildi. Bizning gitlab IP o'zgargan va CI qovoqqa aylangan kungacha ular to'g'ri ishladilar. Muammo shundaki, CIda ishtirok etgan docker tasvirlaridan birida Python modullarini ssh orqali tortib olgan git bor edi. Ssh uchun sizga shaxsiy kalit kerak bo'ladi va ... u taniqli_hosts bilan birga rasmda edi. Va har qanday CI haqiqiy IP va ma'lum_hostlarda ko'rsatilgan IP o'rtasidagi nomuvofiqlik tufayli kalitni tekshirish xatosi bilan muvaffaqiyatsiz tugadi. Mavjud Dockfiles-dan tezda yangi rasm yig'ildi va opsiya qo'shildi StrictHostKeyChecking no. Ammo yomon ta'm saqlanib qoldi va liblarni shaxsiy PyPI omboriga ko'chirish istagi bor edi. Shaxsiy PyPI-ga o'tgandan so'ng, qo'shimcha bonus oddiyroq quvur liniyasi va talablar.txtning oddiy tavsifi edi.

Tanlov amalga oshdi, janoblar!

Biz hamma narsani bulutlarda va Kubernetlarda ishlatamiz va oxirida tashqi xotiraga ega fuqaroligi bo'lmagan konteyner bo'lgan kichik xizmatni olishni xohladik. Xo'sh, biz S3 dan foydalanganimiz sababli, unga ustuvorlik berildi. Va agar iloji bo'lsa, gitlab-da autentifikatsiya bilan (agar kerak bo'lsa, uni o'zingiz qo'shishingiz mumkin).

Tezkor qidiruv bir nechta natijalarni berdi: s3pypi, pypicloud va sholg'om uchun html fayllarini "qo'lda" yaratish varianti. Oxirgi variant o'z-o'zidan yo'qoldi.

s3pypi: Bu S3 xostingidan foydalanish uchun cli. Biz fayllarni yuklaymiz, html-ni yaratamiz va uni bir xil paqirga yuklaymiz. Uyda foydalanish uchun javob beradi.

pypicloud: Bu qiziqarli loyiha bo'lib tuyuldi, lekin hujjatlarni o'qib chiqqach, hafsalam pir bo'ldi. Yaxshi hujjatlar va ehtiyojlaringizga mos ravishda kengaytirish qobiliyatiga qaramay, aslida u ortiqcha va sozlash qiyin bo'lib chiqdi. O'sha paytdagi hisob-kitoblarga ko'ra, sizning vazifalaringizga muvofiq kodni tuzatish uchun 3-5 kun kerak bo'ladi. Xizmat shuningdek ma'lumotlar bazasiga muhtoj. Boshqa hech narsa topmasak, uni qoldirdik.

Chuqurroq qidiruv natijasida Nginx ngx_aws_auth moduli topildi. Uning sinovi natijasi brauzerda S3 paqirining tarkibini ko'rsatadigan XML ko'rsatildi. Qidiruv paytidagi oxirgi jinoyat bir yil oldin sodir bo'lgan. Ombor tashlab ketilgan ko'rinardi.

Manbaga borib o'qish orqali PEP-503 XMLni tezda HTMLga aylantirish va pipga berish mumkinligini angladim. Nginx va S3 haqida biroz ko'proq ma'lumotga ega bo'lganimdan so'ng, men Nginx uchun JS-da yozilgan S3-da autentifikatsiya qilish misoliga duch keldim. Men NJS bilan shunday tanishdim.

Ushbu misolni asos qilib oladigan bo'lsak, bir soat o'tgach, men brauzerimda ngx_aws_auth modulidan foydalanganda bo'lgani kabi bir xil XML-ni ko'rdim, lekin hamma narsa allaqachon JS-da yozilgan.

Menga nginx yechimi juda yoqdi. Birinchidan, yaxshi hujjatlar va ko'plab misollar, ikkinchidan, biz fayllar bilan ishlash uchun Nginx-ning barcha afzalliklarini olamiz (qutidan tashqari), uchinchidan, Nginx uchun konfiguratsiyalarni qanday yozishni biladigan har bir kishi nima ekanligini aniqlay oladi. Nexus haqida gapirmasa ham, Python yoki Go (agar noldan yozilgan bo'lsa) bilan solishtirganda minimalizm ham men uchun ortiqcha.

TL; DR 2 kundan so'ng, PyPi ning sinov versiyasi allaqachon CIda ishlatilgan.

U qanday ishlaydi?

Modul Nginx-ga yuklangan ngx_http_js_module, rasmiy docker tasviriga kiritilgan. Biz direktiv yordamida skriptimizni import qilamiz js_importNginx konfiguratsiyasiga. Funktsiya direktiv tomonidan chaqiriladi js_content. Direktiv o'zgaruvchilarni o'rnatish uchun ishlatiladi js_set, bu argument sifatida faqat skriptda tasvirlangan funksiyani oladi. Lekin biz NJSda pastki so'rovlarni faqat Nginx yordamida bajarishimiz mumkin, XMLHttpRequest emas. Buning uchun Nginx konfiguratsiyasiga mos keladigan joy qo'shilishi kerak. Va skript ushbu manzilga pastki so'rovni tasvirlashi kerak. Nginx konfiguratsiyasidan funktsiyaga kirish uchun funktsiya nomi skriptning o'zida eksport qilinishi kerak export default.

nginx.conf

load_module modules/ngx_http_js_module.so;
http {
  js_import   imported_name  from script.js;

server {
  listen 8080;
  ...
  location = /sub-query {
    internal;

    proxy_pass http://upstream;
  }

  location / {
    js_content imported_name.request;
  }
}

script.js

function request(r) {
  function call_back(resp) {
    // handler's code
    r.return(resp.status, resp.responseBody);
  }

  r.subrequest('/sub-query', { method: r.method }, call_back);
}

export default {request}

Brauzerda so'ralganda http://localhost:8080/ kirib olamiz location /qaysi direktivada js_content funksiyani chaqiradi request skriptimizda tasvirlangan script.js. O'z navbatida, funktsiyada request uchun pastki so'rov amalga oshiriladi location = /sub-query, argumentdan olingan usul bilan (joriy GET misolida). (r), bu funksiya chaqirilganda bilvosita uzatiladi. Subso'rovga javob funksiyada qayta ishlanadi call_back.

S3 sinab ko'rilmoqda

Shaxsiy S3 xotirasiga so'rov yuborish uchun bizga kerak:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Amaldagi http usulidan, joriy sana/vaqt, S3_NAME va URI, SECRET_KEY yordamida imzolangan (HMAC_SHA1) ma'lum turdagi qator yaratiladi. Keyingi qator shunga o'xshash AWS $ACCESS_KEY:$HASH, avtorizatsiya sarlavhasida foydalanish mumkin. Oldingi bosqichda satrni yaratish uchun ishlatilgan sana/vaqt sarlavhaga qo'shilishi kerak X-amz-date. Kodda bu shunday ko'rinadi:

nginx.conf

load_module modules/ngx_http_js_module.so;
http {
  js_import   s3      from     s3.js;

  js_set      $s3_datetime     s3.date_now;
  js_set      $s3_auth         s3.s3_sign;

server {
  listen 8080;
  ...
  location ~* /s3-query/(?<s3_path>.*) {
    internal;

    proxy_set_header    X-amz-date     $s3_datetime;
    proxy_set_header    Authorization  $s3_auth;

    proxy_pass          $s3_endpoint/$s3_path;
  }

  location ~ "^/(?<prefix>[w-]*)[/]?(?<postfix>[w-.]*)$" {
    js_content s3.request;
  }
}

s3.js(AWS Sign v2 avtorizatsiya misoli, eskirgan holatga o'zgartirildi)

var crypt = require('crypto');

var s3_bucket = process.env.S3_BUCKET;
var s3_access_key = process.env.S3_ACCESS_KEY;
var s3_secret_key = process.env.S3_SECRET_KEY;
var _datetime = new Date().toISOString().replace(/[:-]|.d{3}/g, '');

function date_now() {
  return _datetime
}

function s3_sign(r) {
  var s2s = r.method + 'nnnn';

  s2s += `x-amz-date:${date_now()}n`;
  s2s += '/' + s3_bucket;
  s2s += r.uri.endsWith('/') ? '/' : r.variables.s3_path;

  return `AWS ${s3_access_key}:${crypt.createHmac('sha1', s3_secret_key).update(s2s).digest('base64')}`;
}

function request(r) {
  var v = r.variables;

  function call_back(resp) {
    r.return(resp.status, resp.responseBody);
  }

  var _subrequest_uri = r.uri;
  if (r.uri === '/') {
    // root
    _subrequest_uri = '/?delimiter=/';

  } else if (v.prefix !== '' && v.postfix === '') {
    // directory
    var slash = v.prefix.endsWith('/') ? '' : '/';
    _subrequest_uri = '/?prefix=' + v.prefix + slash;
  }

  r.subrequest(`/s3-query${_subrequest_uri}`, { method: r.method }, call_back);
}

export default {request, s3_sign, date_now}

Haqida bir oz tushuntirish _subrequest_uri: bu boshlang'ich uri ga qarab S3 ga so'rovni shakllantiradigan o'zgaruvchidir. Agar siz "ildiz" ning mazmunini olishingiz kerak bo'lsa, siz ajratuvchini ko'rsatuvchi uri so'rovini yaratishingiz kerak. delimiter, bu kataloglarga mos keladigan barcha CommonPrefixes xml elementlari ro'yxatini qaytaradi (PyPI bo'lsa, barcha paketlar ro'yxati). Agar siz ma'lum bir katalogdagi (barcha paket versiyalari ro'yxati) tarkiblar ro'yxatini olishingiz kerak bo'lsa, uri so'rovida katalog (paket) nomi bilan, albatta, slash / bilan tugaydigan prefiks maydoni bo'lishi kerak. Aks holda, masalan, katalog mazmunini so'rashda to'qnashuvlar mumkin. aiohttp-request va aiohttp-requests kataloglari mavjud va agar so'rovda ko'rsatilgan bo'lsa /?prefix=aiohttp-request, keyin javob ikkala katalogning mazmunini o'z ichiga oladi. Agar oxirida chiziq bo'lsa, /?prefix=aiohttp-request/, keyin javob faqat kerakli katalogni o'z ichiga oladi. Va agar biz faylni so'rasak, natijada olingan uri asl nusxadan farq qilmasligi kerak.

Nginx-ni saqlang va qayta ishga tushiring. Brauzerda biz Nginx manzilini kiritamiz, so'rov natijasi XML bo'ladi, masalan:

Kataloglar ro'yxati

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>myback-space</Name>
  <Prefix></Prefix>
  <Marker></Marker>
  <MaxKeys>10000</MaxKeys>
  <Delimiter>/</Delimiter>
  <IsTruncated>false</IsTruncated>
  <CommonPrefixes>
    <Prefix>new/</Prefix>
  </CommonPrefixes>
  <CommonPrefixes>
    <Prefix>old/</Prefix>
  </CommonPrefixes>
</ListBucketResult>

Kataloglar ro'yxatidan sizga faqat elementlar kerak bo'ladi CommonPrefixes.

Brauzerdagi manzilimizga kerakli katalogni qo'shish orqali biz uning mazmunini XML shaklida ham olamiz:

Katalogdagi fayllar ro'yxati

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name> myback-space</Name>
  <Prefix>old/</Prefix>
  <Marker></Marker>
  <MaxKeys>10000</MaxKeys>
  <Delimiter></Delimiter>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>old/giphy.mp4</Key>
    <LastModified>2020-08-21T20:27:46.000Z</LastModified>
    <ETag>&#34;00000000000000000000000000000000-1&#34;</ETag>
    <Size>1350084</Size>
    <Owner>
      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
      <DisplayName></DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>old/hsd-k8s.jpg</Key>
    <LastModified>2020-08-31T16:40:01.000Z</LastModified>
    <ETag>&#34;b2d76df4aeb4493c5456366748218093&#34;</ETag>
    <Size>93183</Size>
    <Owner>
      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
      <DisplayName></DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

Fayllar ro'yxatidan biz faqat elementlarni olamiz Key.

Qolgan narsa, natijada paydo bo'lgan XML-ni tahlil qilish va avval Content-Type sarlavhasini matn/html bilan almashtirgan holda HTML sifatida yuborishdir.

function request(r) {
  var v = r.variables;

  function call_back(resp) {
    var body = resp.responseBody;

    if (r.method !== 'PUT' && resp.status < 400 && v.postfix === '') {
      r.headersOut['Content-Type'] = "text/html; charset=utf-8";
      body = toHTML(body);
    }

    r.return(resp.status, body);
  }
  
  var _subrequest_uri = r.uri;
  ...
}

function toHTML(xml_str) {
  var keysMap = {
    'CommonPrefixes': 'Prefix',
    'Contents': 'Key',
  };

  var pattern = `<k>(?<v>.*?)</k>`;
  var out = [];

  for(var group_key in keysMap) {
    var reS;
    var reGroup = new RegExp(pattern.replace(/k/g, group_key), 'g');

    while(reS = reGroup.exec(xml_str)) {
      var data = new RegExp(pattern.replace(/k/g, keysMap[group_key]), 'g');
      var reValue = data.exec(reS);
      var a_text = '';

      if (group_key === 'CommonPrefixes') {
        a_text = reValue.groups.v.replace(///g, '');
      } else {
        a_text = reValue.groups.v.split('/').slice(-1);
      }

      out.push(`<a href="/uz/${reValue.groups.v}">${a_text}</a>`);
    }
  }

  return '<html><body>n' + out.join('</br>n') + 'n</html></body>'
}

PyPI sinab ko'rilmoqda

Ishlashi ma'lum bo'lgan paketlarda hech narsa buzilmasligini tekshiramiz.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
python3 -m venv venv
. ./venv/bin/activate

# Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π΅ΠΌ Ρ€Π°Π±ΠΎΡ‡ΠΈΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚Ρ‹.
pip download aiohttp

# Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ Π² ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½ΡƒΡŽ Ρ€Π΅ΠΏΡƒ
for wheel in *.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done

rm -f *.whl

# УстанавливаСм ΠΈΠ· ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½ΠΎΠΉ Ρ€Π΅ΠΏΡ‹
pip install aiohttp -i http://localhost:8080

Liboslarimiz bilan takrorlaymiz.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
python3 -m venv venv
. ./venv/bin/activate

pip install setuptools wheel
python setup.py bdist_wheel
for wheel in dist/*.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done

pip install our_pkg --extra-index-url http://localhost:8080

CIda paketni yaratish va yuklash quyidagicha ko'rinadi:

pip install setuptools wheel
python setup.py bdist_wheel

curl -sSfT dist/*.whl -u "gitlab-ci-token:${CI_JOB_TOKEN}" "https://pypi.our-domain.com/${CI_PROJECT_NAME}"

Autentifikatsiya

Gitlab-da tashqi xizmatlarning autentifikatsiyasi/avtorizatsiyasi uchun JWT-dan foydalanish mumkin. Nginx-dagi auth_request direktivasidan foydalanib, biz autentifikatsiya ma'lumotlarini skriptdagi funktsiya chaqiruvini o'z ichiga olgan pastki so'rovga yo'naltiramiz. Skript Gitlab url-ga yana bir quyi so'rov yuboradi va agar autentifikatsiya ma'lumotlari to'g'ri ko'rsatilgan bo'lsa, Gitlab 200 kodini qaytaradi va paketni yuklash/yuklab olishga ruxsat beriladi. Nima uchun bitta quyi so'rovdan foydalanmaslik va ma'lumotlarni darhol Gitlab-ga yuborish kerak? Chunki biz avtorizatsiyaga har qanday o'zgartirish kiritganimizda Nginx konfiguratsiya faylini tahrirlashimiz kerak bo'ladi va bu juda zerikarli vazifa. Bundan tashqari, agar Kubernetes faqat oΚ»qish uchun moΚ»ljallangan ildiz fayl tizimi siyosatidan foydalansa, bu nginx.conf faylini konfiguratsiya xaritasi orqali almashtirishda yanada murakkablashadi. Va Nginx-ni konfiguratsiya xaritasi orqali konfiguratsiya qilish mutlaqo imkonsiz bo'lib qoladi va bir vaqtning o'zida jildlarni (pvc) va faqat o'qish uchun ildiz fayl tizimini (bu ham sodir bo'ladi) ulashni taqiqlovchi siyosatlardan foydalaniladi.

NJS qidiruv dasturidan foydalanib, biz muhit o'zgaruvchilari yordamida nginx konfiguratsiyasida belgilangan parametrlarni o'zgartirish va skriptda ba'zi tekshiruvlarni amalga oshirish imkoniyatiga ega bo'lamiz (masalan, noto'g'ri ko'rsatilgan URL).

nginx.conf

location = /auth-provider {
  internal;

  proxy_pass $auth_url;
}

location = /auth {
  internal;

  proxy_set_header Content-Length "";
  proxy_pass_request_body off;
  js_content auth.auth;
}

location ~ "^/(?<prefix>[w-]*)[/]?(?<postfix>[w-.]*)$" {
  auth_request /auth;

  js_content s3.request;
}

s3.js

var env = process.env;
var env_bool = new RegExp(/[Tt]rue|[Yy]es|[Oo]n|[TtYy]|1/);
var auth_disabled  = env_bool.test(env.DISABLE_AUTH);
var gitlab_url = env.AUTH_URL;

function url() {
  return `${gitlab_url}/jwt/auth?service=container_registry`
}

function auth(r) {
  if (auth_disabled) {
    r.return(202, '{"auth": "disabled"}');
    return null
  }

  r.subrequest('/auth-provider',
                {method: 'GET', body: ''},
                function(res) {
                  r.return(res.status, "");
                });
}

export default {auth, url}

Katta ehtimol bilan savol tug'iladi: -Nega tayyor modullardan foydalanmaslik kerak? U erda hamma narsa allaqachon qilingan! Masalan, var AWS = require('aws-sdk') va S3 autentifikatsiyasi bilan "velosiped" yozishning hojati yo'q!

Keling, kamchiliklarga o'tamiz

Men uchun tashqi JS modullarini import qila olmaslik yoqimsiz, ammo kutilgan xususiyatga aylandi. Yuqoridagi misolda tasvirlangan talab ('kripto') hisoblanadi o'rnatilgan modullar va ular uchun faqat ishlarni talab qiladi. Bundan tashqari, skriptlardan kodni qayta ishlatishning hech qanday usuli yo'q va siz uni turli xil fayllarga nusxalashingiz va joylashtirishingiz kerak. Umid qilamanki, bir kun kelib bu funksiya amalga oshiriladi.

Nginx-dagi joriy loyiha uchun siqishni ham o'chirib qo'yish kerak gzip off;

Chunki NJS-da gzip moduli mavjud emas va uni ulash mumkin emas, shuning uchun siqilgan ma'lumotlar bilan ishlashning iloji yo'q. To'g'ri, bu ish uchun bu haqiqatan ham minus emas. Ko'p matn yo'q va uzatilgan fayllar allaqachon siqilgan va qo'shimcha siqish ularga yordam bermaydi. Bundan tashqari, bu unchalik yuklangan yoki muhim xizmat emaski, siz kontentni bir necha millisekundlarga tezroq yetkazib berish bilan bezovta qilishingiz kerak.

Skriptni disk raskadrovka qilish uzoq vaqt talab etadi va faqat error.log-dagi "chop etish" orqali mumkin. O'rnatilgan ro'yxatga olish darajasi ma'lumotlariga, ogohlantirish yoki xatolikka qarab, mos ravishda r.log, r.warn, r.error 3 ta usuldan foydalanish mumkin. Men Chrome (v8) yoki njs konsol vositasida ba'zi skriptlarni disk raskadrovka qilishga harakat qilaman, lekin u erda hamma narsani tekshirib bo'lmaydi. Kodni disk raskadrovka qilishda, ya'ni funktsional test, tarix quyidagicha ko'rinadi:

docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginx

va bunday ketma-ketliklar yuzlab bo'lishi mumkin.

Ular uchun pastki so'rovlar va o'zgaruvchilar yordamida kod yozish chigal chigalga aylanadi. Ba'zan siz kodingizning harakatlar ketma-ketligini aniqlashga urinib, turli xil IDE oynalari atrofida yugurishni boshlaysiz. Bu qiyin emas, lekin ba'zida bu juda zerikarli.

ES6 uchun to'liq qo'llab-quvvatlash yo'q.

Boshqa kamchiliklar ham bo'lishi mumkin, lekin men boshqa hech narsaga duch kelmadim. Agar NJSdan foydalanishda salbiy tajribangiz bo'lsa, ma'lumot almashing.

xulosa

NJS - bu Nginx-da turli xil JavaScript skriptlarini amalga oshirish imkonini beruvchi engil ochiq manbali tarjimon. Uning rivojlanishi davomida ishlashga katta e'tibor berildi. Albatta, hali ko'p etishmayotgan narsa bor, lekin loyiha kichik jamoa tomonidan ishlab chiqilmoqda va ular faol ravishda yangi xususiyatlarni qo'shib, xatolarni tuzatmoqda. Umid qilamanki, bir kun kelib NJS sizga tashqi modullarni ulash imkonini beradi, bu esa Nginx funksiyasini deyarli cheksiz qiladi. Ammo NGINX Plus mavjud va ehtimol u erda hech qanday xususiyat bo'lmaydi!

Maqola uchun to'liq kod bilan ombor

AWS Sign v4 qo'llab-quvvatlashi bilan njs-pypi

ngx_http_js_module modulining ko'rsatmalari tavsifi

Rasmiy NJS ombori ΠΈ hujjatlar

Dmitriy Volintsevdan NJS dan foydalanish misollari

njs - nginx-da mahalliy JavaScript skripti / Dmitriy Volnyevning Saint HighLoad++ 2019 da nutqi

NJS ishlab chiqarishda / Vasiliy Soshnikovning HighLoad++ 2019 da nutqi

AWS da REST so'rovlarini imzolash va autentifikatsiya qilish

Manba: www.habr.com