Angular ile Elasticsearch Uygulama Örneği

Cem Topkaya
9 min readDec 18, 2020

--

Bu makalede öğrenecekleriniz:

  • stackblitz.com içinde koşan angular uygulamasıyla yerel makine veya uzak makinede koşan elasticsearch sunucusunu konuşturmak için http/https proxy kullanmak
  • CORS hataları neden karşınıza çıkar ve nasıl çözersiniz
  • Elasticsearch içinde güvenlik ayarları (kullanıcı, CORS vs.)
  • Authorization türlerinden en temel olanı Basic Auth nasıl çalışır ve http paketinde kullanıcı adı ve şifresi taşımanın en basit yöntemi
  • ve diğer irili ufaklı bilgi hapları

Bu makaleden sonra kendinden imzalı sertifika nasıl hazırlanır konusuna bakabilirsiniz (var bir iki makale benim de yazdığım).

Elasticsearch içinde bize verilen paket NodeJS tarafında kullanabileceğimiz istemci örneği bu adreste (kırık adresleri haber edin lütfen). Zaten istemcilerin listesini şöyle görüyoruz:

https://www.elastic.co/guide/en/elasticsearch/client/index.html

Ancak Angular uygulama ile Elasticsearch sorguları yapmak için farklı bir paketi kullanacağız:

npm i elasticsearch

Elasticsearch Sunucusunu Kullanıcı Adı ve Parola İle Güvenli Hale Getirmek

Önce komutunu interactive parametresiyle Elasticsearch içinde tanımlı kullanıcılar için şifre koruma oluşturmak istersek (ki bu durumda Angular’dan istek yaparken URL’de kullanıcı adı ve şifresini de girmemiz gerekecek) bu adresi ve aşağıdaki komutu takip edebiliriz.

Ayrıca kibana üstünden Elasticsearch için kullanıcı oluşturmak için bu adresi takip edebilirsiniz.

Ön şartlar:

xpack.security.enabled: true
  • xpack.security.enabled: true Ayarı elasticsearch.yml dosyasında açık olmalı
$ bin/elasticsearch-setup-passwords interactive

Artık varsayılan kullanıcılardan elastic için bir şifre tayin ettiniz ama yeni kullanıcılar eklemek istiyorsanız ***:

bin/elasticsearch-users
([useradd <username>] [-p <password>] [-r <roles>]) |
([list] <username>) |
([passwd <username>] [-p <password>]) |
([roles <username>] [-a <roles>] [-r <roles>]) |
([userdel <username>])

CORS Hatasını Gidermek

Önce hatayı meydana tekrar üretmek için:

  1. elasticsearch.yml içinde varsa CORS ayarları (farklı alan adlarındaki sayfalardan Elasticsearch sunucusuna istek yapmak -cross origin resource sharing-) silinecek

2. Önce Chrome içinde bir yerde localhost:9200 (Elasticsearch varsayılan ayarlarda çalışıyorsa) adresine istek yapalım. Chrome Developer Tools içinde Network sekmesinden bu isteğimizi fetch ile kopyalayalım.

3. Muhtemelen elasticsearch http çalıştığı için yine http adresinde bir sayfada Chrome Developer Tools içinde Console sekmesinde fetch ile kopyaladığımız isteği çalıştıralım. Ama önce http çalışan bir siteyi Google ile arayalım:

fetch("http://localhost:9200/logs/_search", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "tr,en-US;q=0.9,en;q=0.8,ru;q=0.7,la;q=0.6,zh-CN;q=0.5,zh;q=0.4",
"authorization": "Basic ZWxhc3RpYzp0ZXN0MTIz",
"cache-control": "max-age=0",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
});

authorization > Basic Auth

Yukarıdaki istek içindeki authorization anahtarıyla ilgili temel bir bilgiyi paylaşayım. Kullanıcı adımız “elastic” ve şifremiz “test123”. Bu bilgileri authentication (kullanıcı doğrulama) için kullanacağız ancak göndermek için çeşitli yöntemler mevcut ve biz Basic ile bu verinin şeklini (form) değiştirmek için kodlayacağız (encode). Insomnia uygulaması açık kaynak kod olup Postman gibi REST adreslere istek yapabilmemizi sağlıyor. Aşağıdaki ekran görüntülerindeki gibi Basic Auth ile kullanıcı adı ve şifresini yazabiliyoruz ancak elbette HTTP paketine base64 ile kodlanarak yazılıyor.

“Basic Auth” ile girdiğimiz kullanıcı adı ve şifresinin arasına “:” işareti konularak bir metin oluşturuluyor. Sonra bu metin base64 olarak kodlanıyor. Elasticsearch kullanıcı adı ve şifresini internet gezginimizde olan btoa() işleviyle kodlayalım ve Insomnia uygulamasında gördüğümüz Authorization anahtarındaki metni (ZWxhc3RpYzp0ZXN0MTIz) oluşturalım.

var kullaniciAdi = "elastic"
var sifresi = "test123"
var kodlanacak = kullaniciAdi + ":" + sifresi
kodlanmisHali = btoa(kodlanacak)

Aldığımız hata diyor ki; sen http://info.cern.ch' sayfasından 'http://localhost:9200/logs/_search' sayfasına gitmek istiyorsun. Yani farklı kökenlerden (origin) istek yapıyorsun farkında mısın? Yani diyor ki; cern.ch alanından localhost alanına istek yaptığın için internet gezginin Chrome olarak seni önce durdurmak, sonra localhost alanına bir ön ziyaret yaparak “cern.ch” alanından yapılacak ziyaretler için uygun musun diye sormak zorundayım. Eğer seni kabul eder ise bu isteklerine müsade eder, localhost’un vereceği cevapları sana iletirim.

Sanırım CORS olayını daha iyi anlamak için aşağıdaki çizime bakarak algımızı daha iyileştirmiş oluruz ama tafsilatlı bir adres için sizi buraya alabiliriz:

Access to fetch at 'http://localhost:9200/logs/_search' from origin 'http://info.cern.ch' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Peki CORS hatasını nasıl gidereceğiz? Basit cevabı; /config/elasticsearch.yml dosyasına aşağıdaki satırları ekleyelim:

http.cors.enabled : true
http.cors.allow-origin : "*"
http.cors.allow-methods : OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers : X-Requested-With,X-Auth-Token,Content-Type, Content-Length

4. Tekrar Elasticsearch’ü çalıştırıp fetch isteğini konsoldan tekrar gönderdiğimizde bu kez farklı bir hata alacağız:

Access to fetch at 'http://localhost:9200/logs/_search' from origin 'http://info.cern.ch' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

developer.mozilla Adresinde bu credentials nedir hangi parametreler ile gönderilir diye açıklama mevcut. Özetle:

Farklı kaynaklar arası isteklerde isteği yaptığımız alanda tanımlı bilgileri (cookie) diğer etki alanına gönderip göndermeyeceğimizi belirtiyoruz. 3 Farklı değer alıyor:

omit : Çerez göndermez de almaz da.

same-origin : Aynı alan adında ise çerez gönderip alır. Yani cemtopkaya.com adresinde yine cemtopkaya.com/haberler adresine giden bir bağlantıya tıkladıysanız çerez (cookie) bilgileri sunucuya iletilir. Varsayılan olarak bu değer seçilidir.

include : Farklı alan adlarından bu istekleri yapıyorsanız örneğin cemtopkaya.com adresinden tdav.org.tr adresine giden bir istekte cookie bilgilerini de göndermek isterseniz include seçilmeli.

Aşağıdaki örnekte XML Http Request (AJAX) ile bir istek yapalım. Ancak developer.mozilla.org adresinden önce CORS isteklerine kapalı example.com adresine sonra CORS isteklerine açık enable-cors.org adresine.

Yukarıdaki ekran çıktısında önce CORS isteklerine kapalı olan example.com adresine developer.mozilla.org adresinden istek yapıyoruz ve çerez bilgilerinin de gitmesi için withCredential ile gitmek istediğimizde önce CORS’a kapalı hatasını görüyoruz.

CORS isteklerine açık enable-cors.org adresine gitmek istediğimizde önce farklı bir alan adından gelen istekler için enable-cors.org adresinin müsait olup olmadığını soruyor. Öncelikle OPTIONS metoduyla bir ön uçuş (preflight ) tetiklemek için tanınmayan anahtar — değer ikilisini http isteğimizin başlığına (http header) yazıp istek yapalım ki; Chrome gezgini bu tuhaf başlık bilgisi ve farklı alan adından gelecek istek için OPTIONS ile önce sunucuya bir ziyaret yapsın:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://enable-cors.org/', true);
xhr.setRequestHeader('tanimadik_baslik', 'options tetikler');
xhr.withCredentials = false;
xhr.send(null);
Access to XMLHttpRequest at 'https://enable-cors.org/' from origin 'https://developer.mozilla.org' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

Eğer sunucu “her türlü http başlığını kabul ediyorum” (Access-Control-Allow-Headers) deseydi bu OPTIONS ziyareti 200 (HTTP Status OK) ile biterdi.

Access-Control-Allow-Headers: <header-name>[, <header-name>]*
Access-Control-Allow-Headers: *

Ama bizim sıkıntımız 200 alıp almamak değil, enable-cors.org adresinin hangi kaynaklardan (origin) istekleri kabul ediyor olduğunu görmekti. Cevap Response Header içinde access-control-allow-origin anahtarında saklı “*”.

Şimdi noktaları birleştirelim. Her kim olursan ol yine gel (access-control-allow-origin: "*") diyen bir sunucu kapılarını açtığında, internet gezginimiz diyor ki; “sen yine de credential bilgilerini include seçeneğiyle gönderme”. Bunu yukarıda aldığımız şu hatayla görmüştük:

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

credentials Modunun include olduğu bir isteği oluşturmak için AJAX isteğimizde xhr.withCredential = true işaretlemiştik hatırlarsanız. Bu isteği Chrome Developer Tools penceresinin Network sekmesinden fetch şeklinde kopyalamak istediğimizde ise credentials: include olarak gelmişti.

Demek ki xhr.withCredential=false veya fetch şeklindeki isteğimizde credentials özelliğini omit olarak göndereceğiz:

fetch("http://localhost:9200/logs/_search", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "tr,en-US;q=0.9,en;q=0.8,ru;q=0.7,la;q=0.6,zh-CN;q=0.5,zh;q=0.4",
"authorization": "Basic ZWxhc3RpYzp0ZXN0MTIz",
"cache-control": "max-age=0",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});

5. Http başlıklarında geçen tanımadık anahtarlar için OPTIONS uçuşu yapacak ve eğer izin verilmeyen bir http başlık anahtarı varsa CORS hatası üretecektir. Nitekim aşağıdaki fetch ile yaptığımız istekte bir çok başlık bilgisi …/config/elasticsearch.yml dosyasında belirtilmediği için hata alacağız.

Başlık bilgilerini sadeleştirip sadece authorization, cache-control, accept, accept-language anahtarlarını kabul ederek tekrar Elasticsearch uygulamamızı başlatıp şu isteğimizi çalıştıralım:

fetch("http://localhost:9200/logs/_search", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "tr,en-US;q=0.9,en;q=0.8,ru;q=0.7,la;q=0.6,zh-CN;q=0.5,zh;q=0.4",
"authorization": "Basic ZWxhc3RpYzp0ZXN0MTIz",
"cache-control": "max-age=0"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
})
.then(cevap=>cevap.json())
.then(console.log)

Hem preflight (OPTIONS) hem de gerçek isteğimiz (GET ile yaptığımız istek) 200 ile sonuçlandı. Gelen yanıt JSON türünde olacağı için response_nesnesi.json() çağrısıyla JSON nesnesine dönüştürüyor, konsolda yazdırıp yanıtımızı görebiliyoruz.

6. Gelin tüm HTTP başlıklarını Elasticsearch içinde kabul edecek ayarı yaparak bu kez tüm http anahtarlarını ilk gönderdiğimiz gibi gönderelim ve sorunsuz çalıştığını görelim:

fetch("http://localhost:9200/logs/_search", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "tr,en-US;q=0.9,en;q=0.8,ru;q=0.7,la;q=0.6,zh-CN;q=0.5,zh;q=0.4",
"authorization": "Basic ZWxhc3RpYzp0ZXN0MTIz",
"cache-control": "max-age=0",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
}).then(cevap=>cevap.json()).then(console.log)
http.cors.allow-headers : “*”

7. Şimdi httpS://stackblitz.com adresinde geliştireceğimiz Angular uygulamasının http://localhost:9200 adresine yapacağı isteklerde karşılaşacağımız şu hataya bakmadan önce uygulamanın nasıl olacağına bakalım:

8. httpS olan bir siteden http çalışan bir adrese XHR (Ajax) istekleri yaptığımızda aşağıdaki gibi hata alacağız ve eğer istek yaptığımız adresi httpS olarak değiştiremiyorsak nasıl bir çözüm geliştirebileceğimize bakalım:

Mixed Content: The page at ‘https://stackblitz.com/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint ‘http://172.19.0.62:920/'. This request has been blocked; the content must be served over HTTPS.

Çalışabilmesi için stackblitz.com httpS üstünden çalıştığı için ya elasticsearch sunucunuz da httpS üstünden koşmalı veya arada bir httpS çalışan vekil (proxy) çalıştırmalısınız.

Örneğin: simple-https-proxy

https://www.npmjs.com/package/simple-https-proxy

Kurmak için:

npm install -g simple-https-proxy

Çalıştırmak için:

  • Önce SSL sertifikası oluşturun:
simple-https-proxy --makeCerts=true
  • Sonra hedef Elasticsearch sunucunuzun önünde çalıştırmak için:
simple-https-proxy 
--target=http://172.19.0.62:9200
--port=9201
--rewriteBodyUrls=false

Başka bir örnekle:

localhost Yerine test.local alan adını 127.0.0.1 adresine hosts dosyamızda yönlendirelim ve vekil sunucumuzu buna göre tekrar çalıştıralım:

Kapanış için bir cümle: “bölünerek değil birleşerek büyürüz ve büyük olabilmek için her parçamızın daha büyük olması için bilgimizi paylaşmamız gerekir”.

--

--

Cem Topkaya
Cem Topkaya

Written by Cem Topkaya

Evlat, kardeş, ağabey, eş, baba, müzik sever, öğrenmek ister, paylaşmaya can atar, iyi biri olmaya çalışır, hakkı geçenlerden helallik ister vs.

No responses yet