Loomin oma PyPI-repositoorium autoriseerimise ja S3-ga Nginx'is

Selles artiklis tahan jagada oma kogemusi NJS-iga, Nginxi jaoks mõeldud JavaScripti tõlgiga, mida arendab Nginx Inc., tuues välja selle peamised võimalused reaalse näite abil. NJS on JavaScripti alamhulk, mis võimaldab laiendada Nginxi funktsionaalsust. Küsimusele miks oma tõlkija??? vastas Dmitri Volintsev, andes üksikasjaliku selguse. Ühesõnaga: NJS on nginx-way, samas kui JavaScript on progressiivsem, "loodud" ja ei kasuta GC nagu Lua.

A long time ago…

Eelmises töökohas pärandusena sain gitlabi, kus oli mitmeid erinevaid CI/CD torujuhtmeid koos docker-compose'i, dind'i ja muude toredustega, mis viidi kaniko raudtee peale. Varasemalt CI-s kasutatud pildid kolisid oma algses vormis. Need töötasid tõrgeteta seni, kuni meie gitlabi IP muutus ja CI muutus kõrvitsaks. Probleem oli selles, et ühes docker-pildis, mis osales CI-s, oli git, mis tõmbas SSH kaudu Python'i mooduleid. SSH jaoks on vajalik privaatne võti ja … see oli pildis koos known_hosts'iga. Iga CI lõpetas viga võtme kontrollimisel, kuna reaalne IP ei langenud kokku known_hosts'is märgitud IP-iga. Olemasolevatest Dockfile'idest koondati kiiresti uus pilt ja lisati valik StrictHostKeyChecking no. Kuid ebameeldiv maitse jäi ja tekkis soov libe viia privaatsele PyPI hoidla. Lisaeeliseks oli, et pärast üleminekut privaatsele PyPI-le sai torujuhe lihtsam ja requirements.txt'i normaalne kirjeldus.

Valik on tehtud, härrased!

Me kõik keerame asju pilvedes ja Kuberneteses ning tulemuseks soovisime väikest teenust, mis esindaks stateless konteinerit koos välishoidlaga. Kuna kasutame S3, siis oli prioriteet selles suunas. Samuti sooviksime võimalikult autentimist gitlabis (võib vajadusel ise kirjutada).

Kiire otsing andis mitu tulemust: s3pypi, pypicloud ja variant «käsitsi» html-failide loomisele repos. Viimane variant kukkus ise välja.

s3pypi: See on cli S3 majutuse kasutamiseks. Laadime failid üles, genereerime html ja laeme sama baketisse. Koduseks kasutamiseks sobib.

pypicloud: Näis huvitav projekt, kuid pärast dokumentatsiooni lugemist tuli pettumus. Vaatamata heale dokumentatsioonile ja võimalustele oma ülesannete jaoks kohandada, osutus see tegelikult üleliigseks ja keeruliseks seadistada. Koodi oma vajaduste järgi kohandamine võtab hinnanguliselt 3–5 päeva. Samuti vajab teenus andmebaasi. Jätame selle kindlasti kõrvale, kui midagi muud ei leia.

Süvendi otsing andis Nginx'i mooduli, ngx_aws_auth. Selle testimise tulemusena saadi XML, mis kuvati brauseris ja mille kaudu oli näha S3 paki sisu. Viimane commit oli tehtud aasta tagasi. Reposiit nägi välja mahajäetud.

Külastades esmast allikat ja lugedes PEP-503 , mõistsin, et XML-i saab jooksvalt HTML-iks konverteerida ja edastada seda pip. Veel veidi Google'ides Nginx'i ja S3 kohta leidsin näite S3 autentimisest, mis oli kirjutatud JS jaoks Nginx'ile. Nii kohtusin NJS-iga.

Võttes aluseks selle näite, nägin tunni pärast oma brauseris sama XML-i, mis oli saadud ngx_aws_auth mooduli kaudu, kuid kõik oli nüüd kirjutatud JS-is.

Nginx'i lahendus meeldis mulle väga. Esiteks, olemas on hea dokumentatsioon ja palju näiteid; teiseks saame kõik Nginx'i failihalduse eelised (karbis); kolmandaks oskab iga inimene, kes suudab Nginx'i konfiguratsioone kirjutada, aru, kuidas ja mis toimub. Samuti on see minu jaoks pluss, et see on minimalistlik võrreldes Python'i või Go'ga (kui kirjutada nullist), rääkimata nexusest.

TL;DR, kahe päeva pärast oli testiversioon PyPist juba CI-s kasutusel.

Kuidas see töötab?

Nginx'i laaditakse moodul ngx_http_js_module, on lisatud ametlikku docker-ima. Impordime meie skripti kasutades direktiivi js_importNginxi konfiguratsiooni. Funktsiooni kutsumine toimub direktiivi kaudu js_content. Muutujate seadmiseks kasutatakse direktiivi js_set, mis võtab argumendina ainult skriptis kirjeldatud funktsiooni. NJS-s saame alamsõnumeid teha ainult Nginxi kaudu, mitte mingit XMLHTTPRequest'i. Selleks peab Nginxi konfiguratsioonis olema lisatud vastav location. Skriptis peab olema kirjeldatud alamsõnum (subrequest) sellele location'ile. Nginxi konfiguratsioonist funktsiooni kutsumiseks tuleb skripti sees funktsiooni nimi eksportida 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'i kood
    r.return(resp.status, resp.responseBody);
  }

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

export default {request}

Brauseri päringu korral http://localhost:8080/ me jõuame location /kus direktiiv js_content kutsub ellu funktsiooni request on kirjeldatud meie skriptis script.js. Samuti toimub funktsioonis request alamsõnumi tegemine location = /sub-query, meetodi (käesolevas näites GET) kaudu saadud argumentidest (r), mis edastatakse kaudselt, kui seda funktsiooni kutsutakse. Alamküsitluse vastuse töötlemine toimub funktsioonis call_back.

Proovime S3

Privaatsesse S3-hoidusesse päringu tegemiseks on meil vajalikud:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Kasutatav http-meetod, praegune kuupäev/aeg, S3_NAME ja URI genereerivad teatud tüüpi stringi, mis allkirjastatakse (HMAC_SHA1) kasutades SECRET_KEY. Edasi string, mis näeb välja nagu AWS $ACCESS_KEY:$HASH, saab kasutada autoriseerimise päises. Sama kuupäev/aeg, mida kasutati stringi genereerimiseks eelneval sammul, peab olema lisatud päisesse X-amz-date. Koodis näeb see välja nii:

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\/(?.*) {
    internal;

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

    proxy_pass          $s3_endpoint\/$s3_path;
  }

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

s3.js(AWS Sign v2 autoriseerimise näide, muudetud vananenuks)

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}

Veidi selgitust _subrequest_uri: see muutuja, mis sõltuvalt algsest uri'st loob päringu S3-le. Kui on vaja saada sisu «juurest», tuleb selleks luua uri-päring koos eraldajaga eraldaja, mis tagastab nimekirja kõigist xml-elementidest CommonPrefixes, mis vastavad kaustadele (PyPI puhul, nimekiri kõigist paketidest). Kui on vaja saada kindla kausta sisu nimekirja (kõik paketiversioonid), siis peab uri-päring sisaldama valdkonda prefix koos kausta (paketi) nimega, mis peab lõppema kaldkriipsuga /. Vastasel juhul võivad esineda kollisioonid kausta sisu päringus, näiteks. On kaustad aiohttp-request ja aiohttp-requests ja kui päringus on näidatud /?prefix=aiohttp-request, siis vastuses on sisu mõlemast kaustast. Kui aga lõpus on kaldkriips, /?prefix=aiohttp-request/, siis vastuses on ainult vajalik kaust. Ja kui me küsime faili, siis lõplik uri ei tohi erineda algsest.

Salvestame, taaskäivitame Nginxi. Brauseris sisestame meie Nginxi aadressi, päringu töötluse tulemus on XML, näiteks:

Kaustade nimekiri

myback-space
  
  
  10000
  /
  false
  
    new/
  
  
    old/

Kataloogide loendist on vajalikud vaid elemendid CommonPrefixes.

Lisades oma brauserisse meie päevakohase katalooge, saame selle sisu samuti XML-formaadis:

Failide loend kataloogis

myback-space
  old/
  
  10000
  
  false
  
    old/giphy.mp4
    2020-08-21T20:27:46.000Z
    "00000000000000000000000000000000-1"
    1350084
    
      02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4
      
    
    STANDARD
  
  
    old/hsd-k8s.jpg
    2020-08-31T16:40:01.000Z
    "b2d76df4aeb4493c5456366748218093"
    93183
    
      02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4
      
    
    STANDARD

Valige faililoendist ainult elemendid Avaõigus.

Jäänud on XML töötlemine ja edastamine HTML kujul, muutes pealkirja Content-Type väärtuseks text/html.

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="/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Proovime PyPIt

Kontrollime, et kuskil ja miski ei purune testitud paketides.

# Создаем для тестов новое окружение
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

Korrake koos meie teekidega.

# Создаем для тестов новое окружение
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

CI-s näeb paketi loomine ja üleslaadimine välja järgmiselt:

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}"

Autentimine

Gitlab võimaldab kasutada JWT-d välisten teenuste autentimiseks/autoriseerimiseks. Nginx'i auth_request direktiivi kasutades suuname autentimisandmed alamsõnumisse, mis sisaldab funktsiooni kutsumist skripti sees. Skript teeb veel ühe alamsõnumi Gitlab'i url-ile ja kui autentimisandmed on õiged, siis tagastab Gitlab koodi 200 ning võimaldab paketi laadimist/allalaadimist. Miks mitte kasutada ühte alamsõnumit ja saata andmed otse Gitlab'i? Kuna siis tuleks igakord, kui meil on autoriseerimise muutusi, muuta Nginx konfiguratsioonifaili, mis on üsna vaevarikas. Samuti, kui Kubernetes'is kasutatakse read-only root filesystem poliitikat, lisab see veelgi keerukust nginx.conf'i asendamisel configmap'i kaudu. Nginx'i seadistamine configmap'i kaudu on täiesti võimalik, kui samal ajal kasutatakse poliitikaid, mis keelavad mahutite (pvc) ühendamise ja read-only root filesystem'i (see juhtub ka).

Kasutades NJS-i vahelinkina, saame muuta nginx-i konfi sissekirjutatud parameetreid keskkonnamuutujate abil ning teostada skriptis kontrollimisi (näiteks vale URL-i puhul).

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 ~ "^/(?P[w-]*)[/]?(?P[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}

Tõenäoliselt tekib küsimus: -Kuid miks mitte kasutada valmis mooduleid? Seal on ju kõik juba tehtud! Näiteks, var AWS = require('aws-sdk') ja ei pea kirjutama 'ratast' S3-autentimise jaoks!

Liigume miinuste juurde

Minu jaoks oli väline JS-moodulite importimise võimatuse tõttu ebameeldiv, kuid ootuspärane omadus. Ülaltoodud näites kirjeldatud require('crypto') on sisseehitatud moodulid ja töötavad ainult neile. Samuti ei ole võimalik koodi skriptidest taaskasutada ja tuleb seda kopeerida erinevatesse failidesse. Loodan, et kunagi see funktsionaalsus rakendatakse.

Samuti peab Nginx'i jaoks olevat praeguses projektis kokkusurumine keelatud. gzip off;

Sest NJS-is puudub gzip-moodul ja selle ühendamine ei ole võimalik, seetõttu ei saa kokku surutud andmete juurde töötada. Tõsi, see pole antud juhul eriti puudus. Teksti pole palju ja edastatavad failid on juba surutud, seega ei aita täiendav kokkusurumine eriti. Samuti ei ole see nii koormatud ega kriitiline teenus, et muretseda sisu edastamise pärast mõne millisekundi kiiremini.

Skripti tõrkeotsing on pikk ja võimalik ainult läbi "printide" error.log-is. Olenevalt seadistatud logimise tasemest info, warn või error on võimalikud 3 meetodit r.log, r.warn, r.error vastavalt. Proovin mõningaid skripte tõrkeotsida Chrome'is (v8) või konsooli tööriistas njs, kuid mitte kõike ei saa seal kontrollida. Koodi tõrkeotsing, st funktsionaalne testimine, näeb välja umbes nii:

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

ja selliseid järjestusi võib olla sadu.

Koodi kirjutamine alamsüsteemide ja nende jaoks muutujate kasutamisega muutub segaseks sogaks. Aeg-ajalt hakkad hüppama erinevate IDE akende vahel, püüdes mõista oma koodi tegevuste järjekorda. See ei ole keeruline, kuid mõnikord on see väga stressirohke.

ES6 täis toetust ei ole.

Võibolla on veel mõningaid puudusi, kuid ma ei ole millegi muuga kokku puutunud. Jagage infot, kui Teil on negatiivseid kogemusi NJS kasutamisel.

Kokkuvõte

NJS on kerge open-source tõlgendaja, mis võimaldab Nginxis erinevate JavaScripti stsenaariumide rakendamist. Selle arendamisel oli suurt tähelepanu pööratud jõudlusele. Muidugi on selles veel palju puudu, kuid projekt areneb väikeses meeskonnas ja nad lisavad aktiivselt uusi funktsioone ning parandavad vigu. Loodan, et kunagi võimaldab NJS väliste moodulite ühendamist, mis muudab Nginxi funktsionaalsuse praktiliselt piiramatu. Kuid NGINX Plusiga ei pruugi mõningaid funktsioone olla!

Artikli täiskoodi hoidla

njs-pypi AWS Sign v4 toe jaoks

ngx_http_js_module mooduli direktiivide kirjeldus

NJS ametlik hoidla ja dokumentatsioon

Dmitri Volyncev'i NJS-i kasutamise näited

njs — natiivne JavaScript-skriptiimine nginx-is / Выступление Дмитрия Волныева на Saint HighLoad++ 2019

NJS tootmises / Выступление Василия Сошникова на HighLoad++ 2019

REST-päringute allkirjastamine ja autentimine AWS-is

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster