Ağustos 2026

Bir Node.js projesi yazıp sunucuya koyduğunuzda iş bitmiyor aslında yeni başlıyor. Kullandığınız kütüphaneler zamanla eskiyor, içlerinde güvenlik açıkları bulunuyor, ve kodunuzun tek kopyası sunucudaysa bir gün o sunucu bozulduğunda elinizde hiçbir şey kalmayabilir.

Bu yazıda, üç ayrı Node.js projesini nasıl "bakımdan geçirdiğimi" adım adım anlatıyorum: her komutu neden çalıştırdığımı, çıkan sonucu nasıl okuduğumu, ve hangi durumda "dur, burada dikkatli olmam lazım" dediğimi. Terminal komutu daha önce hiç kullanmamış biri bile takip edebilsin diye yazdım.

1. Adım: Projeniz Git'te mi?

Bakıma başlamadan önce sorulacak en temel soru bu. Git, kodunuzun her değişikliğini kaydeden bir zaman makinesi gibi düşünülebilir. Projeniz git'te değilse, sunucunuzun diski bozulduğu ya da yanlışlıkla bir dosyayı sildiğiniz an, o kod bir daha geri gelmez.

Bir klasörün git deposu olup olmadığını şöyle kontrol edersiniz:

cd benim-projem
git status

Eğer "not a git repository" gibi bir hata alıyorsanız, projeniz hiç versiyon kontrolü altında değil demektir. Şöyle başlarsınız:

git init
git add .
git commit -m "İlk sürüm"
Ama dikkat: git add . demeden önce mutlaka bir sonraki adımı (.gitignore) tamamlayın. Yoksa şifrelerinizi, gerçek müşteri verilerinizi, hatta gigabaytlarca gereksiz dosyayı yanlışlıkla kod deposuna gömebilirsiniz — ve bir kere git geçmişine giren bir şeyi tamamen silmek gerçekten zahmetlidir.

.gitignore: Neyi asla depoya almamalı?

Projenizin kök klasöründe .gitignore adında bir dosya, git'e "şunları asla takip etme" der. Bakım yaparken en çok karşıma çıkan, gitignore'a eklenmesi gereken şeyler şunlar:

  • node_modules/ — kütüphanelerin indirildiği klasör. Devasa olabilir (yüzlerce MB) ve package.json'dan her zaman yeniden indirilebilir, o yüzden depoda taşımaya gerek yok.
  • .env — veritabanı şifreniz, API anahtarlarınız gibi gizli bilgilerin durduğu dosya. Bunu asla git'e eklemeyin (private bir depo bile olsa).
  • Gerçek kullanıcı verisi içeren dosyalar — Excel çıktıları, veritabanı dökümleri, yüklenen fotoğraflar. Kod ile veri farklı şeylerdir; veri koda değil, yedeğe gider.
  • Tarayıcı otomasyonu oturum klasörleri — eğer botunuz Puppeteer/Selenium gibi bir araçla tarayıcı açıyorsa, oturum bilgilerinin tutulduğu klasörler hem devasa boyuta ulaşabilir hem de içinde oturum çerezleri (şifre muadili hassas veri) barındırabilir.
  • Çöküş dökümleri (core dosyaları) — bir program çöktüğünde işletim sisteminin bıraktığı, genelde yüz megabaytları bulan dosyalar. Koda ait değiller, silinebilirler.

Örnek bir .gitignore:

node_modules/
.env
core
*.xlsx
logs/
oturum_verisi_*/

Bunu ekledikten sonra git add . çalıştırıp git status ile "hangi dosyalar işleme eklenecek" listesine bakın. Beklemediğiniz büyüklükte bir dosya ya da isim görürseniz, önce onu neden gördüğünüzü anlayın, sonra devam edin.

Kodun bir kopyası da GitHub'da dursun

Git, tek başına sadece yerel bir geçmiş tutar — sunucunun diski giderse o geçmiş de gider. Bu yüzden kodu ayrıca GitHub gibi bir yere de gönderin (push edin). Gerçek kullanıcı verisi barındırmayan ama iş mantığınızı içeren bir proje için depoyu private (özel) yapmayı unutmayın — bu sayede kod sizin kontrolünüzde kalır ama internete açık olmaz.


2. Adım: Kütüphaneleriniz Ne Kadar Eski?

Bir Node.js projesi, package.json dosyasında listelenen onlarca-yüzlerce dış kütüphaneye dayanır. Bunlar zamanla güncellenir; siz güncellemezseniz projeniz "donmuş" bir zaman diliminde kalır. Mevcut durumu görmek için:

npm outdated

Bu komut size üç sütun gösterir:

  • Current — şu an kurulu olan sürüm.
  • Wantedpackage.json'daki kurala göre (örn. ^3.1.0) izin verilen en güncel sürüm.
  • Latest — paketin gerçekten en güncel sürümü (Wanted'dan büyük olabilir, çünkü bazen kural o kadar ileriye izin vermez).

Burada bilmeniz gereken bir kavram var: semver (semantic versioning). Bir sürüm numarası 4.19.2 gibi üç parçadan oluşur:

ParçaAnlamıKırılma riski
4.19.2 — MajorBüyük, uyumsuz değişikliklerYüksek — kodunuz bozulabilir
4.19.2 — MinorYeni özellik, geriye uyumluDüşük
4.19.2 — PatchHata düzeltmesiNeredeyse yok

Yani npm outdated çıktısında Current ile Wanted arasındaki fark küçükse (patch/minor), rahatça güncelleyebilirsiniz. Current ile Latest arasında bir major fark varsa (örneğin 4.x'ten 5.x'e), acele etmeyin — o kütüphanenin "neler değişti" (changelog) sayfasına bakmadan güncellemeyin.

Basit güncelleme

Sadece patch/minor farkları için:

npm update

Bu, package.json'daki kurallara sadık kalarak (majör sürüm atlamadan) her şeyi en güncel izin verilen sürüme çeker.


3. Adım: Güvenlik Açıklarını Bulmak

Kütüphanelerin sadece "eski" olması bir sorun değildir; asıl önemlisi içlerinde bilinen bir güvenlik açığı olup olmadığıdır. Bunu şöyle öğrenirsiniz:

npm audit

Çıktı size her açığı bir önem derecesiyle listeler: low → moderate → high → critical. En altta da bir özet görürsünüz, örneğin:

12 vulnerabilities (4 moderate, 7 high, 1 critical)

Bu rakamı görünce panik yapmayın — genelde göründüğünden çok daha az kaynağa iniyor. Detaylı, makine tarafından okunabilir hâlini görmek için:

npm audit --json

"via" ne demek? Zincirleme bağımlılık

Az önce yaşadığım gerçek bir örnekle anlatayım. Bir projede npm audit 12 açık buldu. İlk bakışta korkutucu görünüyordu: sequelize (bir veritabanı kütüphanesi), exceljs (Excel dosyası oluşturan kütüphane), connect-session-sequelize (oturum yönetimi) hepsi "açıklı" görünüyordu.

Ama detaya bakınca gerçek tablo şuydu: bu üç kütüphanenin kendisinde hiçbir sorun yoktu — hepsi zaten en güncel sürümdeydi. Sorun, bu kütüphanelerin arka planda kullandığı, çok daha küçük ve genelde fark edilmeyen dört pakette gizliydi: tar, nanoid, brace-expansion, uuid. Yani sequelize, kendi içinde eski bir uuid sürümü taşıdığı için "açıklı" görünüyordu — sequelize'in kendi kodunda bir sorun yoktu.

Bunu anlamanın yolu, her açığın via (üzerinden, aracılığıyla) alanına bakmak:

sequelize (moderate)
  -> via dep: uuid

connect-session-sequelize (moderate)
  -> via dep: sequelize

Bu, bir zincir gösteriyor: connect-session-sequelizesequelize'e bağımlı → sequelize da uuid'e bağımlı → asıl sorun uuid'de. Zinciri takip ettiğinizde, kaç "açık" göründüğü değil, kaç gerçek kaynak olduğu ortaya çıkıyor. Az önceki örnekte 12 görünen açık, aslında sadece 4 kaynağa iniyordu.

"fixAvailable" alanına dikkat — kör kör güncellemeyin

npm audit her açık için bir "düzeltme mevcut mu" bilgisi de verir. İki türü var:

tar | critical | fixAvailable: true

puppeteer | high | fixAvailable: {"name":"puppeteer","version":"19.8.0","isSemVerMajor":true}

Birinci örnekte fixAvailable: true — bu, "majör sürüm değiştirmeden, package.json kurallarına sadık kalarak güvenle düzeltebilirim" demek. İkincisinde ise bir obje var ve içinde "isSemVerMajor":true yazıyor — bu da "düzeltmek için bu paketi başka bir majör sürüme geçirmem lazım" demek.

Burada durup düşünün. npm bazen "düzeltme" olarak paketi ileri değil geriye götürmenizi önerir — çünkü kendi bağımlılık çözücüsü, "hiç açık kalmasın" diye bazen paketi eskiye, açığın henüz eklenmediği bir sürüme çekmeyi tercih eder. Gerçek bir örnekte npm bana sequelize'i güncel 6.x sürümünden 3.x'e (yıllar öncesine) düşürmemi önerdi. Bunu kör kör kabul etseydim, tüm veritabanı kodum çalışmaz hâle gelirdi. npm audit fix --force komutu tam olarak bunu yapar — sormadan, "en kolay" çözümü uygular. Production'da asla ilk elden kullanmayın.

4. Adım: Güvenli Bir Şekilde Düzeltme

İşte gerçekte izlediğim, güvenli sıra:

Önce, force'suz dene

npm audit fix

Bu, sadece fixAvailable: true olan (majör değiştirmeyen) açıkları düzeltir. Genelde açık sayınızı ciddi oranda azaltır, hiçbir şeyi kırma riski neredeyse sıfırdır.

Kalanlar için: "overrides" ile hedefli müdahale

Yukarıdaki uuid örneğine dönelim. sequelize zaten en güncel sürümdeydi ama içinde hâlâ eski bir uuid taşıyordu ve npm bunu "düzeltmek" için sequelize'i düşürmemi öneriyordu. Bunun yerine package.json'a şunu ekledim:

{
  "overrides": {
    "uuid": "^11.1.1"
  }
}

Bu satır npm'e şunu söyler: "Kim uuid isterse istesin, ona en az 11.1.1 sürümünü ver — package.json'da öyle yazmasa bile." npm install çalıştırdığınızda bu, tüm bağımlılık ağacında zorla uygulanır. uuid gibi API'si çok stabil, basit paketlerde bu genelde güvenlidir (temel fonksiyonlar sürümler arası neredeyse hiç değişmez). Karmaşık, çok özellikli bir pakette (örneğin bir web framework'ünde) aynı şeyi yapmadan önce daha dikkatli olun.

Gerçekten düzeltmesi olmayan açıklar da var

Bazen karşınıza çıkan açığın hiçbir güncel çözümü olmadığını görürsünüz. Bunu anlamanın yolu basit — paketin gerçekten en güncel sürümünün ne olduğuna bakmak:

npm view paket-adi version

Eğer bu komutun döndürdüğü sürüm, sizde zaten kurulu olan sürümle aynıysa — yani "en güncel" dediği paket zaten elinizdeki paketse — o açığın gerçekten upstream'de (paketi yazan kişide) henüz bir çözümü yok demektir. Bu durumda iki seçeneğiniz var:

  1. Alternatif bir pakete geçmek (varsa ve geçiş maliyeti makulse).
  2. Riski bilinçli olarak kabul etmek — ama bunu sessizce değil, gerekçesiyle not ederek yapın: bu açık ne işlemiyor, gerçekte kimden gelen veriyi işliyor, kötüye kullanılma ihtimali ne kadar gerçekçi?

Örneğin bir projede extract-zip paketinde çözümsüz bir "sembolik link" açığı buldum. Ama bu paket sadece kendi sunucumun, güvendiğim bir kaynaktan (tarayıcı yazılımının resmi indirme adresinden) indirdiği bir dosyayı açmak için kullanılıyordu — dışarıdan, bir kullanıcının yüklediği bir dosyayı değil. Yani gerçek saldırı yüzeyi çok dardı. Bunu riski kabul edip ilerleyerek not aldım; eğer aynı paket kullanıcıdan gelen dosyaları açıyor olsaydı, karar tamamen farklı olurdu.


5. Adım: Değiştirdikten Sonra Doğrulayın — "Dua Etmeyin"

Bir bağımlılığı güncelledikten sonra iş bitmiyor. Şunları yapmadan "tamamdır" demeyin:

  1. Söz dizimi kontrolü. Dosyada bariz bir yazım hatası var mı, hızlıca bakın:
    node --check src/server.js
  2. Uygulamayı yeniden başlatın. Sunucunuzda uygulamaları pm2 gibi bir "process manager" ile çalıştırıyorsanız:
    pm2 restart uygulama-adi
  3. Hata loglarına bakın. Yeniden başlattıktan hemen sonra:
    pm2 logs uygulama-adi --err --lines 30
    Burada veritabanı bağlantı hatası, "bulunamadı" hatası gibi bir şey görürseniz, güncelleme bir şeyi kırmış demektir.
  4. Gerçek bir işlemi deneyin. Uygulamanızda bir sayfayı açın, bir formu gönderin, bir kaydı kaydedin — "çalışıyor" demeden önce gerçekten çalıştığını görün. Loglarda "Veritabanı bağlantısı başarılı" gibi olumlu bir satır ya da gerçek bir isteğin (200 OK) geçtiğini görmek, en güvenilir kanıttır.
İpucu: Değişiklik yapmadan hemen önce package.json ve package-lock.json'un bir kopyasını alın (cp package.json package.json.yedek). Bir şeyler ters giderse eski hâline dönmek saniyeler sürer. Zaten git kullanıyorsanız bu adım gereksiz — git checkout -- package.json package-lock.json aynı işi görür.

6. Adım: Loglarınız Diskinizi Doldurmasın

Uzun süre çalışan bir sunucu, sürekli log (kayıt) dosyası üretir. Bunlara dokunmazsanız bir gün diskiniz dolar ve tüm sunucu çöker. pm2 kullanıyorsanız, pm2-logrotate modülünü kurup şu ayarları yapmak yeterli:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30

Bu, her log dosyası 10MB'ı geçtiğinde otomatik olarak döndürür (eskisini sıkıştırıp yeni bir dosyaya geçer) ve en fazla 30 günlük geçmiş tutar. Kurduktan sonra unutabilirsiniz — arka planda kendi kendine çalışır.


7. Adım: Yedekleme — Git Tek Başına Yeterli Değil

Git, kodunuzu korur ama veritabanınızı korumaz. Kullanıcı kayıtları, işlem geçmişi gibi veriler ayrı bir yedekleme gerektirir. Basit bir başlangıç: sunucunuzda bir cron job (zamanlanmış görev) ile her gece veritabanının bir dökümünü almak:

# crontab -e ile açıp şu satırı eklemek, her gece 03:00'te yedek alır
0 3 * * * mysqldump -u kullanici -p'sifre' veritabani_adi > /yedekler/$(date +\%F).sql

Bu yedeklerin de sunucunun dışında bir yerde durması önemli — sunucu tamamen kaybolursa, yedek de onunla beraber gitmesin diye.


Özet: Bakım Kontrol Listesi

Elinizdeki her Node.js projesi için, ayda bir bu adımları takip edin:

  1. ☐ Proje git'te mi, son değişiklikler push edildi mi?
  2. .gitignore güncel mi — yeni eklenen bir klasör/dosya türü var mı?
  3. npm outdated — patch/minor farkları için npm update.
  4. npm audit — kaç gerçek kaynak var, kaçında "fixAvailable: true"?
  5. ☐ Force'suz npm audit fix ile güvenli olanları temizle.
  6. ☐ Kalanlar için: majör sürüm gerektiren her paketi tek tek değerlendir, kör kör --force kullanma.
  7. ☐ Değişiklik sonrası: syntax kontrolü, restart, log kontrolü, gerçek işlem testi.
  8. ☐ Log rotasyonu kurulu mu, disk dolmuyor mu?
  9. ☐ Veritabanı yedeği düzenli alınıyor mu, sunucu dışında bir yerde duruyor mu?

Bu adımların hiçbiri tek başına karmaşık değil — asıl beceri, hangi sonucun güvenle uygulanabileceğini, hangisinin dikkat istediğini ayırt edebilmek. Bir sonraki bakımınızda, her "majör sürüm" uyarısında durup bir nefes alın, önce ne değiştiğini okuyun, sonra karar verin.


nginx ile "Dağıtık" Saldırılara Karşı Alt Ağ Bazlı Rate Limiting

Ya da: 70 farklı IP'den gelen ama hiçbiri tek başına şüpheli görünmeyen bir saldırıyı nasıl durdururuz?

Bir gün sunucum aniden yavaşlamaya başladı. Log'lara baktığımda klasik bir "tek kötü IP" görüntüsü yoktu — bunun yerine aynı /24 bloğundan (yani "192.168.1.X" tarzı, sadece son rakamı değişen) 70'e yakın farklı IP, her biri kendi başına makul sayıda istek atıyordu. Hiçbiri tek başına alarm verecek kadar agresif değildi, ama hepsi birden sunucuyu dize getiriyordu.

Bu yazıda, bu tarz "dağıtık ama düşük yoğunluklu" saldırılara karşı nginx'in kendi imkanlarıyla (üçüncü parti araç, Cloudflare falan olmadan) nasıl önlem alınacağını, sıfırdan anlatıyorum.


1. Önce temel: nginx'te "hız sınırlama" nasıl çalışır?

nginx, gelen istekleri bir kovaya damlayan su gibi düşünür (buna "leaky bucket" algoritması denir). Şöyle bir kural tanımlarsın:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

Türkçesi: "Her IP için ayrı bir kova aç (`$binary_remote_addr` = IP adresi). Bu kova saniyede 10 isteklik bir akışı sindirebilsin. Bütün kovaları tutmak için 10MB'lık bir bellek alanı ayır (`zone=mylimit:10m`)."

Sonra bu kuralı bir yere uygulaman lazım:

location / {
    limit_req zone=mylimit burst=20 nodelay;
}
  • burst=20: ani patlamalarda 20 isteklik ekstra tampon tanı (yoksa gerçek bir kullanıcının açtığı sayfadaki 5-6 paralel istek bile anında engellenir).
  • nodelay: bu tampondaki istekleri bekletmeden hemen işle, sadece tampon dolunca reddet. (Bunu koymazsan nginx istekleri sıraya koyup yavaşça işler — bu da gerçek kullanıcıyı fark ettirmeden yavaşlatır.)

Eşik aşılınca nginx varsayılan olarak 503 Service Unavailable döner.

2. Peki neden bu bazen yetmiyor?

Kritik nokta şu: kova, anahtar ne ise ona göre açılır. Yukarıdaki örnekte anahtar $binary_remote_addr, yani IP adresi. Bu demek oluyor ki:

100 farklı IP saldırıyorsa, nginx 100 ayrı kova açar. Hiçbiri diğerini etkilemez. Saldırgan, her IP'yi eşiğin biraz altında tutarak bu korumayı tamamen atlatabilir — ki botnet'ler zaten genelde böyle çalışır.

Benim yaşadığım durum tam olarak buydu: 70 farklı IP, hepsi aynı /24 bloğundan (örnek: 57.141.0.3, 57.141.0.61, 57.141.0.64...), hiçbiri tek başına eşiği geçmiyordu.

3. Çözüm: map ile "sahte" bir ortak anahtar üretmek

İhtiyacımız olan şey: 70 farklı IP'nin aynı kovaya düşmesi. nginx'te "bu IP'nin /24'ü nedir" diye hazır bir değişken yok, ama map direktifiyle kendimiz üretebiliyoruz:

map $remote_addr $subnet_key {
    default                                   $remote_addr;
    "~^(?<net>\d+\.\d+\.\d+)\.\d+$"            $net;
}

Bu satır korkutucu görünebilir ama parça parça çok basit. Bir IP adresi üzerinden (57.141.0.71) soldan sağa gidelim:

ParçaAnlamı
~"Bu bir düz metin değil, bir regex kalıbı" (nginx map'e özel bir işaret)
^ ... $"Metnin başından sonuna kadar tamamen eşleşsin"
\d"Bir rakam" (0-9)
\d+"Bir veya daha fazla rakam"
\."Gerçek bir nokta karakteri" (düz . regex'te "her şey" demek, o yüzden \ ile "gerçekten nokta" yapıyoruz)
(?<net>...)"Bu parantezin içinde eşleşeni bir kenara ayır, adını net koy, sonra $net diye kullanayım"

Şimdi bunu 57.141.0.71 üzerine uygulayalım:

^                → başlangıç
\d+              → "57"
\.               → "."
\d+              → "141"
\.               → "."
\d+              → "0"      ┐
                             ├─ bunlar (?<net>...) İÇİNDE → net = "57.141.0"
\.               → "."      ┘   (parantez burada kapanıyor)
\d+              → "71"     ← SON rakam grubu parantez DIŞINDA, yani atılıyor
$                → bitiş

Sonuç: $net = "57.141.0" — son oktet (.71) kırpıldı. 57.141.0.3 için de aynı işlemi yapsan yine "57.141.0" çıkar. İşte tam istediğimiz şey: son rakamı ne olursa olsun, aynı /24 bloğundaki her IP artık aynı $subnet_key'e sahip.

Genel kural olarak aklında kalsın: \d+ = rakam(lar), \. = gerçek nokta, (?<isim>...) = "bu kısmı işaretle", ^...$ = "baştan sona tam eşleşsin". Bu dört kalıp, günlük regex ihtiyaçlarının büyük kısmını karşılar.

4. Hepsini birleştirmek

Artık $subnet_key değişkenimiz var, onu normal limit_req_zone'da anahtar olarak kullanabiliriz:

http {
    # ... nginx.conf'un http{} bloğu içinde, herhangi bir server{} bloğunun dışında:

    map $remote_addr $subnet_key {
        default                                   $remote_addr;
        "~^(?<net>\d+\.\d+\.\d+)\.\d+$"            $net;
    }

    limit_req_zone $subnet_key zone=subnet_limit:10m rate=30r/s;

    server {
        server_name example.com;

        location / {
            limit_req zone=subnet_limit burst=60 nodelay;
            # ... geri kalan config'in (proxy_pass vb.) aynen kalıyor
        }
    }
}

Not: map ve limit_req_zone sadece http{} seviyesinde tanımlanabilir (server/location içinde olmaz). limit_req (uygulama satırı) ise istediğin server{} veya location{} içine yazılır — böylece sadece saldırı gördüğün siteyi etkiler, diğerlerine dokunmaz.

5. Neden her yere değil, tek tek siteye uyguluyoruz?

Aynı sunucuda birden fazla site barındırıyorsan, bu kuralı hepsine birden http{} seviyesinde yazmak cazip gelebilir — teknik olarak mümkün, çünkü nginx ayarları üstten alta miras alınır. Ama önermiyorum, çünkü:

  • Her sitenin trafik "kişiliği" farklıdır. Bir webmail sayfası sürekli arka planda "yenile" isteği atar, bir blog sayfası öyle çalışmaz — tek bir sayı ikisine de adil oturmaz.
  • Yanlış ayarın etki alanı. Rakamları ilk seferde muhtemelen tam doğru tutturamazsın (kalibrasyon zaman alır). Global yazarsan bir hata TÜM siteleri aynı anda etkiler; tek siteye yazarsan hata payı orada sınırlı kalır.
  • Sadece kanıt olan yere uygula. Saldırı gördüğün siteye özel önlem al; saldırı görmediğin siteye körlemesine kısıtlama koyup gerçek kullanıcıları riske atma.

6. Uygularken dikkat edilecekler

  1. Önce mutlaka syntax testi yap:
    nginx -t
    "syntax is ok" / "test is successful" görmeden asla reload etme.
  2. Reload et (restart değil):
    service nginx reload
    Reload, aktif bağlantıları kesmeden config'i tazeler.
  3. İlk birkaç gün error log'unu izle:
    grep "limiting requests" /var/log/nginx/error.log
    Bu satırlar, kuralın gerçekten kimi/ne zaman engellediğini gösterir. Gerçek kullanıcılar burada görünmeye başlarsa, rate/burst değerlerini yukarı çek. Saldırı hâlâ geçiyorsa aşağı çek.

Özet

Klasik IP-bazlı rate limiting, saldırı tek bir kaynaktan geldiğinde işe yarar. Saldırı onlarca/yüzlerce IP'ye dağıtılmışsa (ki modern botnet'ler genelde böyle çalışır), tek çare, isteği hangi IP attığına değil, hangi ağ bloğuna ait olduğuna göre saymaktır. Bunun için nginx'e hazır bir özellik yok, ama map direktifiyle üç-beş satırlık bir regex'le kendi çözümünü yazabiliyorsun — üçüncü parti bir servise ihtiyaç duymadan, tamamen kendi sunucunda.

Author Name

İletişim Formu

Ad

E-posta *

Mesaj *

Blogger tarafından desteklenmektedir.