Nginx’in en az konuşulan ama en güçlü özelliklerinden biri, gelen ya da giden HTTP içeriğini sunucu tarafında değiştirebilmesi. Özellikle ters proxy (reverse proxy) senaryolarında, kaynak uygulamaya dokunmadan HTML çıktısını manipüle etmek bazen hayat kurtarıcı oluyor. sub_filter modülü tam olarak bunu yapıyor: yanıt gövdesindeki metni bulup başka bir metinle değiştiriyor. Kulağa basit geliyor ama doğru kullanıldığında ciddi problemleri çözüyor.
sub_filter Nedir ve Ne Zaman Kullanılır?
sub_filter, Nginx’in ngx_http_sub_module modülü tarafından sağlanan bir direktiftir. Ters proxy arkasındaki bir uygulama sunucusundan gelen yanıtı, istemciye iletmeden önce değiştirmenizi sağlar. Modül çoğu dağıtımda Nginx ile birlikte derlenerek gelir, ama bunu önce doğrulamak gerekir.
Gerçek dünyada bu modülü ne zaman kullanırsınız?
- Eski bir uygulama
http://ile hardcode edilmiş URL’ler döndürüyor, siz ise HTTPS’e geçtiniz - Üçüncü taraf bir yazılım kendi domain adını HTML içine gömüyor, siz farklı bir domain üzerinden yayınlıyorsunuz
- Her sayfaya JavaScript ya da analitik kodu enjekte etmek istiyorsunuz, ama uygulamaya erişiminiz yok
- Uygulamanın döndürdüğü bazı hassas bilgileri (versiyon numaraları, sunucu bilgileri) response’dan temizlemek istiyorsunuz
- A/B test amaçlı belirli sayfa elementlerini değiştirmeniz gerekiyor
Bunların hepsi sub_filter ile halledilebilir. Şimdi kurulumdan başlayalım.
Modülün Mevcut Olup Olmadığını Kontrol Etme
Nginx binary’nizin bu modülle derlenip derlenmediğini şu komutla kontrol edin:
nginx -V 2>&1 | grep -o with-http_sub_module
Eğer çıktıda with-http_sub_module görüyorsanız hazırsınız demektir. Görmüyorsanız iki seçeneğiniz var: dağıtımın sunduğu paketi yeniden seçmek ya da Nginx’i kaynak koddan derlemek.
Ubuntu/Debian sistemlerde nginx-extras paketi genellikle bu modülü içerir:
# Mevcut Nginx kurulumunu kaldırmadan önce versiyonu not alın
nginx -v
# nginx-extras paketini yükle
apt install nginx-extras
# Modülün yüklendiğini doğrula
nginx -V 2>&1 | grep with-http_sub_module
Temel Kullanım ve Sözdizimi
sub_filter direktifinin sözdizimi oldukça sade:
sub_filter 'aranan_metin' 'yeni_metin';
Basit bir örnek ile başlayalım. Diyelim ki ters proxy yapılandırmanızda backend sunucusu http://internal-app.local döndürüyor ama siz https://myapp.example.com üzerinden hizmet veriyorsunuz:
server {
listen 443 ssl;
server_name myapp.example.com;
ssl_certificate /etc/ssl/certs/myapp.crt;
ssl_certificate_key /etc/ssl/private/myapp.key;
location / {
proxy_pass http://internal-app.local:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# HTTP URL'leri HTTPS ile değiştir
sub_filter 'http://internal-app.local' 'https://myapp.example.com';
sub_filter_once off;
}
}
Burada sub_filter_once off önemli. Varsayılan olarak Nginx yalnızca ilk eşleşmede değişiklik yapar. off yaparak tüm eşleşmelerin değiştirilmesini sağlıyoruz.
sub_filter Direktiflerinin Detaylı Açıklaması
Modülle birlikte gelen direktifler şunlar:
sub_filter: Temel arama-değiştirme işlemi yapar. İkinci parametre boş string olursa bulunan metin silinir.
sub_filter_once: Varsayılan değeri on‘dur. off yapıldığında sayfadaki tüm eşleşmeler değiştirilir.
sub_filter_last_modified: Varsayılan off‘tur. on yapıldığında değiştirilen yanıtlarda Last-Modified header’ı korunur.
sub_filter_types: Hangi MIME type’larında değiştirme yapılacağını belirtir. Varsayılan sadece text/html‘dir. Diğer tipler için açıkça belirtmek gerekir.
Birden Fazla Kural Tanımlama
Tek bir location bloğu içinde birden fazla sub_filter kuralı tanımlayabilirsiniz:
location / {
proxy_pass http://legacy-app:3000;
# Eski domain adını yenisiyle değiştir
sub_filter 'href="http://old-domain.com' 'href="https://new-domain.com';
sub_filter 'src="http://old-domain.com' 'src="https://new-domain.com';
sub_filter 'action="http://old-domain.com' 'action="https://new-domain.com';
# Eski şirket adını yenisiyle değiştir
sub_filter 'Acme Corporation' 'GlobalTech Inc.';
# Tüm eşleşmelerde değişiklik yap
sub_filter_once off;
# Proxy header'ları
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Dikkat etmeniz gereken bir nokta: Nginx’in bazı eski versiyonlarında aynı location bloğunda yalnızca iki sub_filter direktifine izin veriliyordu. Nginx 1.9.4 ve sonrasında bu kısıtlama kaldırıldı. Versiyonunuzu nginx -v ile kontrol edin.
Gzip ile Birlikte Kullanım
sub_filter‘ın en sık karşılaşılan sorunlarından biri, backend’in sıkıştırılmış (gzip) yanıt göndermesidir. Nginx sıkıştırılmış içeriği doğrudan değiştiremez. Bu durumu aşmak için proxy tarafında sıkıştırmayı devre dışı bırakmanız ve yanıt alındıktan sonra tekrar sıkıştırmanız gerekir:
server {
listen 80;
server_name proxy.example.com;
# Nginx tarafında sıkıştırma aç
gzip on;
gzip_types text/html text/css application/javascript;
location / {
proxy_pass http://backend-server:8080;
# Backend'den gelen sıkıştırmayı reddet
proxy_set_header Accept-Encoding "";
# İçerik değiştirme kuralları
sub_filter '</head>' '<script src="/analytics.js"></script></head>';
sub_filter_once on;
sub_filter_types text/html;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
proxy_set_header Accept-Encoding "" direktifi ile backend’e “sıkıştırılmış içerik istemiyorum” demiş oluyoruz. Backend düz metin (plain text) yanıt gönderiyor, Nginx bunu değiştiriyor, ardından istemciye kendi gzip sıkıştırmasıyla gönderiyor.
Gerçek Dünya Senaryosu 1: JavaScript Enjeksiyonu
Ekibinizdeki geliştirici erişiminiz olmayan üçüncü taraf bir SaaS paneli var ve siz her sayfaya kendi izleme kodunuzu eklemek istiyorsunuz. sub_filter burada biçilmiş kaftan:
server {
listen 443 ssl;
server_name dashboard.company.com;
ssl_certificate /etc/ssl/certs/dashboard.crt;
ssl_certificate_key /etc/ssl/private/dashboard.key;
location / {
proxy_pass https://saas-vendor.com;
proxy_set_header Host saas-vendor.com;
proxy_set_header Accept-Encoding "";
proxy_ssl_server_name on;
# Kapanış body etiketinden önce script ekle
sub_filter '</body>' '
<script>
(function() {
window._tracker = window._tracker || [];
window._tracker.push({"company": "CompanyName", "env": "prod"});
})();
</script>
</body>';
sub_filter_once on;
sub_filter_types text/html;
}
# Kendi statik dosyalarını sun
location /internal-assets/ {
alias /var/www/internal-assets/;
}
}
Bu yaklaşımın güzel yanı, enjekte ettiğiniz script’i /internal-assets/ altından servis edebilmeniz. Üçüncü taraf sisteme tek bir satır bile dokunmadan kendi kodunuzu her sayfaya yerleştirmiş oluyorsunuz.
Gerçek Dünya Senaryosu 2: Ortam Göstergeleri
Geliştirme, test ve prodüksiyon ortamlarını aynı uygulama kodu üzerinde çalıştırıyorsunuz ve ekibin hangi ortamda olduğunu görsel olarak anlaması gerekiyor. Her ortam için ayrı Nginx yapılandırması ile bunu kolayca çözebilirsiniz:
# /etc/nginx/conf.d/staging.conf
server {
listen 80;
server_name staging.example.com;
location / {
proxy_pass http://app-server:8080;
proxy_set_header Accept-Encoding "";
proxy_set_header Host $host;
# Staging ortam göstergesi ekle
sub_filter '<body>' '<body>
<div style="
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff6b35;
color: white;
text-align: center;
padding: 4px;
font-family: monospace;
font-size: 12px;
z-index: 99999;
">
STAGING ENVIRONMENT - staging.example.com
</div>';
sub_filter_once on;
}
}
Bu yöntemle geliştirici veya test ekibi yanlışlıkla prodüksiyon yerine staging üzerinde çalıştığını anında görüyor. Uygulamanın koduna dokunmadan, saf Nginx yapılandırmasıyla bunu sağlamak oldukça temiz bir çözüm.
Gerçek Dünya Senaryosu 3: Hassas Bilgileri Gizleme
Bazı eski uygulamalar, response içinde versiyon numarası, sunucu ismi ya da başka hassas teknik bilgiler döndürebiliyor. Bunu sub_filter ile temizleyebilirsiniz:
location / {
proxy_pass http://legacy-erp:8080;
proxy_set_header Accept-Encoding "";
# Versiyon bilgisini gizle
sub_filter 'Powered by LegacyERP v2.3.1' 'Enterprise Platform';
sub_filter 'Server: Apache/2.2.31' '';
sub_filter '<!-- Debug: SQL query took 234ms -->' '';
sub_filter '[email protected]' '[email protected]';
sub_filter_once off;
sub_filter_types text/html application/json;
}
Özellikle JSON yanıtlarında da değişiklik yapmak istiyorsanız sub_filter_types direktifine application/json eklemeyi unutmayın.
Değişken Kullanımı ile Dinamik Değiştirme
sub_filter direktifinin ikinci parametresinde Nginx değişkenleri kullanabilirsiniz. Bu özellik, içeriği dinamik olarak değiştirmek için oldukça güçlü:
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://backend:3000;
proxy_set_header Accept-Encoding "";
# Gerçek kullanıcı IP'sini sayfaya göm
sub_filter '<!-- REAL_IP_PLACEHOLDER -->' '$remote_addr';
# Sunucu adını dinamik olarak ekle
sub_filter '<!-- SERVER_NAME -->' '$server_name';
# Request ID ekle (izlenebilirlik için)
sub_filter '</head>' '<meta name="x-request-id" content="$request_id"></head>';
sub_filter_once on;
}
}
Ancak şunu belirtmek gerekir: değişken kullanımı, Nginx’in bu yanıtları önbelleğe almamasına yol açar. Eğer proxy_cache kullanıyorsanız ve değişken bazlı sub_filter uyguluyorsanız cache bypass stratejinizi buna göre ayarlamalısınız.
Performans Considerations
sub_filter kullanırken performans konusunda bilinçli olmak gerekiyor. Büyük HTML sayfalarında her sub_filter kuralı için Nginx tüm yanıt gövdesini taramak zorunda kalıyor. Birkaç pratik tavsiye:
sub_filter_once on kullanabildiğiniz durumlarda kullanın. Nginx ilk eşleşmeden sonra taramayı durduruyor, bu da büyük sayfalarda ciddi performans farkı yaratıyor.
Çok sayıda sub_filter kuralı yerine mümkünse daha az ama daha kapsamlı kurallar yazın.
sub_filter_types direktifini dikkatli kullanın. Wildcard * kullanmak mümkün ama tüm yanıt tiplerini taramak gereksiz CPU kullanımı demek:
# Kötü yaklaşım - her şeyi tara
sub_filter_types *;
# İyi yaklaşım - sadece gerekli tipler
sub_filter_types text/html text/xml application/xhtml+xml;
Büyük/Küçük Harf Duyarlılığı
Varsayılan olarak sub_filter büyük/küçük harf duyarlıdır (case-sensitive). Eğer büyük/küçük harf fark etmeksizin değiştirme yapmak istiyorsanız ne yapabilirsiniz? Maalesef sub_filter‘da yerleşik case-insensitive mod yok. Bu durumda iki seçenek var:
Birincisi, her varyasyon için ayrı kural yazmak:
sub_filter 'legacy corp' 'Modern Inc.';
sub_filter 'Legacy Corp' 'Modern Inc.';
sub_filter 'LEGACY CORP' 'MODERN INC.';
sub_filter 'Legacy corp' 'Modern Inc.';
İkincisi, lua-nginx-module (OpenResty) kullanmak. Eğer OpenResty kullanıyorsanız Lua ile çok daha esnek dönüşümler yapabilirsiniz. Ama bu ayrı bir yazı konusu.
Önbellekleme ile Birlikte Kullanım
sub_filter ve proxy_cache birlikte kullanıldığında dikkat edilmesi gereken bir incelik var: önbelleğe alınan yanıt, sub_filter uygulandıktan sonraki haliyle mi saklanıyor yoksa önceki haliyle mi?
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g;
server {
listen 80;
server_name cached-app.example.com;
location / {
proxy_pass http://backend:8080;
proxy_cache app_cache;
proxy_cache_valid 200 10m;
proxy_set_header Accept-Encoding "";
# sub_filter önbellekten önce uygulanır
# yani önbellekte değiştirilmiş hali saklanır
sub_filter 'OLD_DOMAIN.com' 'NEW_DOMAIN.com';
sub_filter_once off;
}
}
Nginx’in davranışı şu şekilde: yanıt backend’den ilk kez geldiğinde sub_filter uygulanır ve değiştirilmiş hali önbelleğe yazılır. Sonraki isteklerde önbellekten servis edildiğinde sub_filter tekrar çalışmaz. Bu genellikle istenen davranış, ancak dinamik değişken kullanan sub_filter varsa durum farklı.
Hata Ayıklama ve Test
Yapılandırmanızın doğru çalışıp çalışmadığını test etmek için birkaç yöntem:
# Nginx yapılandırmasını syntax açısından kontrol et
nginx -t
# Değişiklik yaptıktan sonra graceful reload
nginx -s reload
# curl ile yanıtı kontrol et (gzip olmadan)
curl -H "Accept-Encoding: identity" http://localhost/test-page | grep "aranan_metin"
# Değişikliğin gerçekleşip gerçekleşmediğini doğrula
curl -s http://localhost/ | grep -i "new_domain.com"
# Debug için access log'a ek bilgi ekle
# nginx.conf içinde log_format'a $upstream_http_content_type ekle
curl -v http://localhost/ 2>&1 | grep -i content-type
Eğer sub_filter çalışmıyorsa en yaygın sebepler şunlar:
- Backend gzip sıkıştırılmış yanıt gönderiyor (
proxy_set_header Accept-Encoding ""eklemeyi unutmuşsunuz) - MIME type eşleşmiyor (
sub_filter_typesayarını kontrol edin) sub_filter_once onmodunda aranan metin sayfada sadece ikinci kez geçiyor- Aranan metinde özel karakter var ve kaçış karakteri kullanılmamış
Nginx Hata Loglarını Aktif Etme
Sorun giderme sırasında debug log seviyesi işe yarayabilir:
# /etc/nginx/nginx.conf içinde error_log seviyesini değiştir
error_log /var/log/nginx/error.log debug;
# Reload et
nginx -s reload
# Log'ları takip et
tail -f /var/log/nginx/error.log | grep -i "sub_filter|filter"
Prodüksiyonda debug log seviyesini açmayın, performans ciddiye alınacak derecede düşer. Test ortamında veya kısa süreli troubleshooting için kullanın.
Güvenlik Notları
sub_filter kullanırken birkaç güvenlik noktasına dikkat etmek gerekiyor.
Enjekte ettiğiniz içeriğin, kullanıcı girdisinden gelen bir değişken içermediğinden emin olun. Nginx değişkenleri $query_string, $arg_* gibi kullanıcı kontrollü veriler içerebilir. Bunları doğrudan sub_filter çıktısında kullanmak XSS açığına yol açabilir.
Ayrıca sub_filter ile büyük miktarda içerik enjekte ederken Content-Security-Policy header’larını da güncellemeyi unutmayın:
location / {
proxy_pass http://backend:8080;
proxy_set_header Accept-Encoding "";
# Script enjekte ediyoruz, CSP'yi güncelle
sub_filter '</body>' '<script src="/tracking.js"></script></body>';
sub_filter_once on;
# CSP header'ını güncelle
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
}
Sonuç
sub_filter, Nginx’in kütüphanesindeki gerçek bir altın araç. Özellikle legacy uygulamaları modernize ederken, üçüncü taraf sistemleri entegre ederken ya da mimari değişiklikler geçiş sürecindeyken hayatı kolaylaştırıyor. HTTP -> HTTPS geçişleri, domain değişiklikleri, ortam işaretleme, analitik kod enjeksiyonu gibi pek çok senaryoda uygulamaya tek satır dokunmadan çözüm üretebiliyorsunuz.
Kullanırken hatırlamanız gereken üç temel nokta var: proxy_set_header Accept-Encoding "" ile backend sıkıştırmasını devre dışı bırakın, sub_filter_types ile doğru MIME tiplerini belirtin ve sub_filter_once davranışını senaryonuza göre ayarlayın. Bu üçünü doğru yaparsanız geri kalanı neredeyse otomatik çalışıyor.
Prodüksiyon ortamında uygulamadan önce mutlaka staging’de test edin ve yanıt sürelerini izleyin. Çok sayıda kural ve büyük yanıt boyutlarıyla performans üzerindeki etkiyi ölçmek, sürprizlerle karşılaşmamanızı sağlar. İyi yapılandırmalar dilerim.