Ech hunn mäin eegene PyPI Repository mat Autorisatioun a S3 gemaach. Op Nginx

An dësem Artikel wëll ech meng Erfahrung mat NJS deelen, e JavaScript Dolmetscher fir Nginx entwéckelt vun Nginx Inc, seng Haaptfäegkeeten mat engem richtege Beispill ze beschreiwen. NJS ass en Ënnerdeel vu JavaScript deen Iech erlaabt d'Funktionalitéit vun Nginx ze verlängeren. Zu der Fro firwat Ären eegenen Dolmetscher??? Den Dmitry Volyntsev huet am Detail geäntwert. Kuerz gesot: NJS ass nginx-Wee, a JavaScript ass méi progressiv, "native" an ouni GC, am Géigesaz zu Lua.

Viru laanger Zäit…

Bei menger leschter Aarbecht ierf ech gitlab mat enger Rei vun motley CI / CD Pipelines mat docker-compose, dind an aner Freed, déi op Kaniko Schinne transferéiert goufen. D'Biller, déi virdru am CI benotzt goufen, goufen an hirer ursprénglecher Form geplënnert. Si hunn richteg geschafft bis den Dag wou eis Gitlab IP geännert huet an CI zu engem Kürbis verwandelt huet. De Problem war datt ee vun den Docker-Biller, déi un CI deelgeholl hunn, git hat, wat Python Moduler iwwer ssh gezunn huet. Fir ssh braucht Dir e private Schlëssel an ... et war am Bild zesumme mat known_hosts. An all CI huet mat engem Schlësselverifizéierungsfehler gescheitert wéinst engem Mëssverständnis tëscht der realer IP an der spezifizéierter an known_hosts. En neit Bild gouf séier aus den existente Dockfiles zesummegesat an d'Optioun gouf bäigefüügt StrictHostKeyChecking no. Awer de schlechte Goût blouf an et war e Wonsch d'Libs an e private PyPI Repository ze réckelen. En zousätzleche Bonus, nom Wiessel op private PyPI, war eng méi einfach Pipeline an eng normal Beschreiwung vun requirements.txt

De Choix ass gemaach, Hären!

Mir lafen alles an de Wolleken a Kubernetes, an um Enn wollte mir e klenge Service kréien, deen e stateless Container mat externer Späichere war. Ee, well mir benotzen S3, Prioritéit war et. A wa méiglech, mat Authentifikatioun am gitlab (Dir kënnt et selwer addéieren wann néideg).

Eng séier Sich huet e puer Resultater geliwwert: s3pypi, pypicloud an eng Optioun mat "manueller" Schafung vun HTML Dateien fir Rüben. Déi lescht Optioun ass vu sech selwer verschwonnen.

s3pypi: Dëst ass e Cli fir S3 Hosting ze benotzen. Mir lueden d'Dateien erop, generéieren den HTML an lued se an deeselwechten Eemer erop. Gëeegent fir doheem benotzen.

pypicloud: Et huet ausgesinn wéi en interessante Projet, awer nodeems ech d'Dokumentatioun gelies hunn, war ech enttäuscht. Trotz gudder Dokumentatioun an der Fäegkeet fir auszebauen fir Äre Besoinen ze passen, huet et an der Realitéit iwwerflësseg a schwéier ze konfiguréieren. De Code ze korrigéieren fir Är Aufgaben ze passen, no Schätzungen zu där Zäit, hätt 3-5 Deeg gedauert. De Service brauch och eng Datebank. Mir hunn et verlooss am Fall wou mir näischt anescht fonnt hunn.

Eng méi déif Sich huet e Modul fir Nginx, ngx_aws_auth. D'Resultat vu sengem Test war XML am Browser ugewisen, deen den Inhalt vum S3 Eemer gewisen huet. Déi lescht Verpflichtung zur Zäit vun der Sich war virun engem Joer. De Repository huet opginn ausgesinn.

Andeems Dir op d'Quell geet a liesen PEP-503 eng Ech hu gemierkt datt XML an HTML op der Flucht ëmgewandelt ka ginn an op Pip ginn. Nodeems ech e bësse méi iwwer Nginx a S3 gegoogelt hunn, sinn ech op e Beispill vun der Authentifikatioun am S3 geschriwwen, geschriwwen an JS fir Nginx. Dat ass wéi ech NJS kennegeléiert hunn.

Wann ech dëst Beispill als Basis huelen, hunn ech eng Stonn méi spéit a mengem Browser deeselwechten XML gesinn wéi wann Dir de Modul ngx_aws_auth benotzt, awer alles war schonn am JS geschriwwen.

Ech hunn d'nginx Léisung wierklech gär. Éischtens, gutt Dokumentatioun a vill Beispiller, zweetens, mir kréien all Goodies vun Nginx fir mat Fichieren ze schaffen (aus der Këscht), drëttens, jiddereen dee weess wéi Configuratioune fir Nginx ze schreiwen wäert kënnen erauszefannen wat ass wat. De Minimalismus ass och e Plus fir mech, am Verglach mam Python oder Go (wann vun Null geschriwwe gëtt), fir den Nexus net ze ernimmen.

TL;DR No 2 Deeg gouf d'Testversioun vu PyPi schonn am CI benotzt.

Wéi heescht et schaffen?

De Modul gëtt an Nginx gelueden ngx_http_js_module, am offiziellen Docker-Bild abegraff. Mir importéieren eise Skript mat der Direktiv js_importzu Nginx Konfiguratioun. D'Funktioun gëtt vun enger Direktiv genannt js_content. D'Direktiv gëtt benotzt fir Variabelen ze setzen js_set, déi als Argument nëmmen d'Funktioun am Skript beschriwwen hëlt. Awer mir kënnen Subqueries an NJS ausféieren nëmme mat Nginx, net all XMLHttpRequest. Fir dëst ze maachen, muss déi entspriechend Plaz an d'Nginx Konfiguratioun bäigefüügt ginn. An de Skript muss eng Ënnerufro op dëser Plaz beschreiwen. Fir Zougang zu enger Funktioun aus der Nginx Config ze kréien, muss de Funktiounsnumm am Skript selwer exportéiert ginn 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}

Wann am Browser gefrot http://localhost:8080/ mir kommen an location /an deem d'Direktiv js_content rifft eng Funktioun request an eisem Skript beschriwwen script.js. Am Tour, an der Funktioun request eng Ënnerquery gëtt gemaach location = /sub-query, mat enger Method (am aktuelle Beispill GET) aus dem Argument kritt (r), implizit passéiert wann dës Funktioun genannt gëtt. D'Ënnerfro Äntwert gëtt an der Funktioun veraarbecht call_back.

Probéiert S3

Fir eng Ufro un de private S3 Stockage ze maachen, brauche mir:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Vun der benotzt http Method, dem aktuellen Datum / Zäit, S3_NAME an URI, gëtt eng gewëssen Zort String generéiert, déi ënnerschriwwe gëtt (HMAC_SHA1) mat SECRET_KEY. Als nächst ass eng Linn wéi AWS $ACCESS_KEY:$HASH, kann an der Autorisatioun Header benotzt ginn. Dee selwechten Datum / Zäit, dee benotzt gouf fir de String am virege Schrëtt ze generéieren, muss an den Header bäigefüügt ginn X-amz-date. Am Code gesäit et esou aus:

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 Autorisatioun Beispill, geännert op deprecated Status)

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}

Eng kleng Erklärung iwwer _subrequest_uri: Dëst ass eng Variabel déi, ofhängeg vum initialen Uri, eng Ufro un S3 formt. Wann Dir den Inhalt vun der "root" muss kréien, da musst Dir eng Uri Ufro erstellen, déi den Delimiter ugeet delimiter, déi eng Lëscht vun all CommonPrefixes xml Elementer zréckginn, entspriechend Verzeichnisser (am Fall vu PyPI, eng Lëscht vun alle Packagen). Wann Dir eng Lëscht vun den Inhalter an engem spezifesche Verzeechnes (eng Lëscht vun all Package Versiounen) muss kréien, da muss d'Uri Ufro e Präfixfeld mat dem Numm vum Verzeechnes (Package) enthalen, deen onbedéngt mat engem Slash / endet. Soss sinn Kollisiounen méiglech wann Dir zum Beispill den Inhalt vun engem Verzeechnes ufrot. Et gi Verzeechnes aiohttp-Request an aiohttp-Requests a wann d'Ufro spezifizéiert /?prefix=aiohttp-request, da wäert d'Äntwert den Inhalt vu béide Verzeichnisser enthalen. Wann et e Slash um Enn ass, /?prefix=aiohttp-request/, da wäert d'Äntwert nëmmen déi néideg Verzeechnes enthalen. A wa mir e Fichier ufroen, da sollt de resultéierende Uri net vun der ursprénglecher ënnerscheeden.

Späichert a restart Nginx. Am Browser gitt mir d'Adress vun eisem Nginx, d'Resultat vun der Ufro gëtt XML, zum Beispill:

Lëscht vun Telefonsbicher

<?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>

Vun der Lëscht vun de Verzeichnisser braucht Dir nëmmen d'Elementer CommonPrefixes.

Andeems Dir de Verzeechnes, dee mir brauche fir eis Adress am Browser ze addéieren, kréie mir och säin Inhalt an XML Form:

Lëscht vun Dateien an engem Verzeechnes

<?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>

Vun der Lëscht vun de Fichier wäerte mir nëmmen Elementer huelen Key.

Alles wat bleift ass de resultéierende XML ze analyséieren an als HTML eraus ze schécken, nodeems Dir als éischt den Content-Type Header mat Text / HTML ersat huet.

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

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

Probéiert PyPI

Mir kontrolléieren datt näischt iwwerall op Packagen brécht, déi bekannt sinn ze schaffen.

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

Mir widderhuelen mat eise Libs.

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

Am CI, e Package erstellen an ze lueden gesäit esou aus:

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

Authentifikatioun

Am Gitlab ass et méiglech JWT fir Authentifikatioun / Autorisatioun vun externen Servicer ze benotzen. Mat der Auth_request Direktiv an Nginx, wäerte mir d'Authentifikatiounsdaten op eng Ënnerrequest redirectéieren, déi e Funktiounsruff am Skript enthält. De Skript wäert eng aner Ënnerfro un d'Gitlab URL maachen a wann d'Authentifikatiounsdaten korrekt spezifizéiert goufen, da gitt Gitlab de Code 200 zréck an den Eroplueden / Download vum Package gëtt erlaabt. Firwat net eng Ënnerquery benotzen an d'Donnéeën direkt op Gitlab schécken? Well da musse mir d'Nginx Konfiguratiounsdatei änneren all Kéier wann mir Ännerungen an der Autorisatioun maachen, an dëst ass eng zimlech tedious Aufgab. Och wann Kubernetes eng liesen-nëmmen Root Dateisystem Politik benotzt, da füügt dëst nach méi Komplexitéit un wann Dir nginx.conf iwwer Configmap ersetzt. An et gëtt absolut onméiglech fir Nginx iwwer Configmap ze konfiguréieren a gläichzäiteg Politiken ze benotzen déi d'Verbindung vu Volumen (pvc) a Read-only Root Dateiesystem verbidden (dëst geschitt och).

Mat der NJS Zwëschenzäit kréie mir d'Méiglechkeet déi spezifizéiert Parameteren an der nginx Configuratioun mat Ëmfeldvariablen z'änneren an e puer Kontrollen am Skript ze maachen (zum Beispill eng falsch spezifizéiert 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}

Wahrscheinlech ass d'Fro gebrach: -Firwat net fäerdeg Moduler benotzen? Do ass schonn alles gemaach! Zum Beispill, var AWS = require('aws-sdk') an et ass net néideg e "Vëlo" mat S3 Authentifikatioun ze schreiwen!

Loosst eis op d'Nodeeler goen

Fir mech ass d'Onméiglechkeet fir extern JS Moduler z'importéieren eng désagréabel, awer erwaart Feature. Beschriwwen am Beispill hei uewen verlaangen ('crypto') ass gebaut-an Moduler a verlaangen nëmmen Wierker fir si. Et gëtt och kee Wee fir Code vu Scripten ze benotzen an Dir musst et a verschidde Dateien kopéieren a pechen. Ech hoffen, datt iergendwann dës Funktionalitéit ëmgesat gëtt.

Kompressioun muss och fir den aktuelle Projet am Nginx deaktivéiert ginn gzip off;

Well et kee gzip Modul an NJS gëtt an et ass onméiglech et ze verbannen; dofir gëtt et kee Wee fir mat kompriméierten Donnéeën ze schaffen. Richteg, dëst ass net wierklech e Minus fir dëse Fall. Et gëtt net vill Text, an déi transferéiert Dateie si scho kompriméiert an zousätzlech Kompressioun hëlleft hinnen net vill. Och dëst ass net sou e geluedenen oder kriteschen Service datt Dir Iech maache musst mat Inhalt e puer Millisekonnen méi séier ze liwweren.

Debugging vum Skript dauert laang an ass nëmme méiglech duerch "Prints" am error.log. Ofhängeg vun der agestallte Loggingsniveau Info, Warnung oder Feeler, ass et méiglech 3 Methoden r.log, r.warn, r.error respektiv ze benotzen. Ech probéieren e puer Scripten am Chrome (v8) oder dem njs Konsol Tool ze debuggen, awer net alles kann do gepréift ginn. Beim Debugging Code, aka funktionell Testen, gesäit d'Geschicht sou eppes aus:

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

an et kann Honnerte vun esou Sequenzen ginn.

Schreiwen Code mat Ënnersufroen a Variablen fir si verwandelt sech an e tangled Tangle. Heiansdo fänkt Dir un verschidden IDE Fënsteren ze rennen a probéiert d'Sequenz vun Aktiounen vun Ärem Code erauszefannen. Et ass net schwéier, awer heiansdo ass et ganz lästeg.

Et gëtt keng voll Ënnerstëtzung fir ES6.

Et kann e puer aner Mängel ginn, mee ech hunn näischt anescht begéint. Deelt Informatioun wann Dir negativ Erfahrung mat NJS hutt.

Konklusioun

NJS ass e liichte Open-Source Dolmetscher deen Iech erlaabt verschidde JavaScript Scripten an Nginx ëmzesetzen. Wärend senger Entwécklung gouf grouss Opmierksamkeet op d'Leeschtung bezuelt. Natierlech fehlt et nach vill, awer de Projet gëtt vun engem klengen Team entwéckelt a si fügen aktiv nei Features derbäi a fixen Bugs. Ech hoffen, datt enges Daags NJS erlaabt Iech extern Moduler ze verbannen, wat d'Nginx Funktionalitéit bal onlimitéiert mécht. Awer et gëtt NGINX Plus a wahrscheinlech gëtt et keng Features!

Repository mat voller Code fir den Artikel

njs-pypi mat AWS Sign v4 Ënnerstëtzung

Beschreiwung vun den Direktiven vum ngx_http_js_module Modul

Offiziell NJS Repository и Dokumentatioun

Beispiller fir NJS vum Dmitry Volintsev ze benotzen

njs - gebierteg JavaScript Scripting an nginx / Ried vum Dmitry Volnyev um Saint HighLoad++ 2019

NJS an der Produktioun / Ried vum Vasily Soshnikov um HighLoad++ 2019

Ënnerschreiwe an authentifizéieren REST Ufroen an AWS

Source: will.com